mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
refactor: unify whatsapp identity handling
This commit is contained in:
@@ -8,7 +8,8 @@ import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { defaultRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveOAuthDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import type { WebChannel } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { jidToE164, resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveComparableIdentity, type WhatsAppSelfIdentity } from "./identity.js";
|
||||
|
||||
export function resolveDefaultWebAuthDir(): string {
|
||||
return path.join(resolveOAuthDir(), "whatsapp", DEFAULT_ACCOUNT_ID);
|
||||
@@ -154,15 +155,51 @@ export function readWebSelfId(authDir: string = resolveDefaultWebAuthDir()) {
|
||||
try {
|
||||
const credsPath = resolveWebCredsPath(resolveUserPath(authDir));
|
||||
if (!fsSync.existsSync(credsPath)) {
|
||||
return { e164: null, jid: null } as const;
|
||||
return { e164: null, jid: null, lid: null } as const;
|
||||
}
|
||||
const raw = fsSync.readFileSync(credsPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined;
|
||||
const jid = parsed?.me?.id ?? null;
|
||||
const e164 = jid ? jidToE164(jid, { authDir }) : null;
|
||||
return { e164, jid } as const;
|
||||
const parsed = JSON.parse(raw) as { me?: { id?: string; lid?: string } } | undefined;
|
||||
const identity = resolveComparableIdentity(
|
||||
{
|
||||
jid: parsed?.me?.id ?? null,
|
||||
lid: parsed?.me?.lid ?? null,
|
||||
},
|
||||
authDir,
|
||||
);
|
||||
return {
|
||||
e164: identity.e164 ?? null,
|
||||
jid: identity.jid ?? null,
|
||||
lid: identity.lid ?? null,
|
||||
} as const;
|
||||
} catch {
|
||||
return { e164: null, jid: null } as const;
|
||||
return { e164: null, jid: null, lid: null } as const;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readWebSelfIdentity(
|
||||
authDir: string = resolveDefaultWebAuthDir(),
|
||||
fallback?: { id?: string | null; lid?: string | null } | null,
|
||||
): Promise<WhatsAppSelfIdentity> {
|
||||
const resolvedAuthDir = resolveUserPath(authDir);
|
||||
maybeRestoreCredsFromBackup(resolvedAuthDir);
|
||||
try {
|
||||
const raw = await fs.readFile(resolveWebCredsPath(resolvedAuthDir), "utf-8");
|
||||
const parsed = JSON.parse(raw) as { me?: { id?: string; lid?: string } } | undefined;
|
||||
return resolveComparableIdentity(
|
||||
{
|
||||
jid: parsed?.me?.id ?? null,
|
||||
lid: parsed?.me?.lid ?? null,
|
||||
},
|
||||
resolvedAuthDir,
|
||||
);
|
||||
} catch {
|
||||
return resolveComparableIdentity(
|
||||
{
|
||||
jid: fallback?.id ?? null,
|
||||
lid: fallback?.lid ?? null,
|
||||
},
|
||||
resolvedAuthDir,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +222,14 @@ export function logWebSelfId(
|
||||
includeChannelPrefix = false,
|
||||
) {
|
||||
// Human-friendly log of the currently linked personal web session.
|
||||
const { e164, jid } = readWebSelfId(authDir);
|
||||
const details = e164 || jid ? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}` : "unknown";
|
||||
const { e164, jid, lid } = readWebSelfId(authDir);
|
||||
const parts = [jid ? `jid ${jid}` : null, lid ? `lid ${lid}` : null].filter(
|
||||
(value): value is string => Boolean(value),
|
||||
);
|
||||
const details =
|
||||
e164 || parts.length > 0
|
||||
? `${e164 ?? "unknown"}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}`
|
||||
: "unknown";
|
||||
const prefix = includeChannelPrefix ? "Web Channel: " : "";
|
||||
runtime.log(info(`${prefix}${details}`));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { buildMentionRegexes, normalizeMentionText } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { isSelfChatMode, jidToE164, normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { isSelfChatMode, normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
getComparableIdentityValues,
|
||||
getMentionIdentities,
|
||||
getSelfIdentity,
|
||||
identitiesOverlap,
|
||||
type WhatsAppIdentity,
|
||||
} from "../identity.js";
|
||||
import type { WebInboundMsg } from "./types.js";
|
||||
|
||||
export type MentionConfig = {
|
||||
@@ -9,9 +16,8 @@ export type MentionConfig = {
|
||||
};
|
||||
|
||||
export type MentionTargets = {
|
||||
normalizedMentions: string[];
|
||||
selfE164: string | null;
|
||||
selfJid: string | null;
|
||||
normalizedMentions: WhatsAppIdentity[];
|
||||
self: WhatsAppIdentity;
|
||||
};
|
||||
|
||||
export function buildMentionConfig(
|
||||
@@ -23,13 +29,9 @@ export function buildMentionConfig(
|
||||
}
|
||||
|
||||
export function resolveMentionTargets(msg: WebInboundMsg, authDir?: string): MentionTargets {
|
||||
const jidOptions = authDir ? { authDir } : undefined;
|
||||
const normalizedMentions = msg.mentionedJids?.length
|
||||
? msg.mentionedJids.map((jid) => jidToE164(jid, jidOptions) ?? jid).filter(Boolean)
|
||||
: [];
|
||||
const selfE164 = msg.selfE164 ?? (msg.selfJid ? jidToE164(msg.selfJid, jidOptions) : null);
|
||||
const selfJid = msg.selfJid ? msg.selfJid.replace(/:\\d+/, "") : null;
|
||||
return { normalizedMentions, selfE164, selfJid };
|
||||
const normalizedMentions = getMentionIdentities(msg, authDir);
|
||||
const self = getSelfIdentity(msg, authDir);
|
||||
return { normalizedMentions, self };
|
||||
}
|
||||
|
||||
export function isBotMentionedFromTargets(
|
||||
@@ -41,16 +43,12 @@ export function isBotMentionedFromTargets(
|
||||
// Remove zero-width and directionality markers WhatsApp injects around display names
|
||||
normalizeMentionText(text);
|
||||
|
||||
const isSelfChat = isSelfChatMode(targets.selfE164, mentionCfg.allowFrom);
|
||||
const isSelfChat = isSelfChatMode(targets.self.e164, mentionCfg.allowFrom);
|
||||
|
||||
const hasMentions = (msg.mentionedJids?.length ?? 0) > 0;
|
||||
const hasMentions = targets.normalizedMentions.length > 0;
|
||||
if (hasMentions && !isSelfChat) {
|
||||
if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) {
|
||||
return true;
|
||||
}
|
||||
if (targets.selfJid) {
|
||||
// Some mentions use the bare JID; match on E.164 to be safe.
|
||||
if (targets.normalizedMentions.includes(targets.selfJid)) {
|
||||
for (const mention of targets.normalizedMentions) {
|
||||
if (identitiesOverlap(targets.self, mention)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -65,8 +63,8 @@ export function isBotMentionedFromTargets(
|
||||
}
|
||||
|
||||
// Fallback: detect body containing our own number (with or without +, spacing)
|
||||
if (targets.selfE164) {
|
||||
const selfDigits = targets.selfE164.replace(/\D/g, "");
|
||||
if (targets.self.e164) {
|
||||
const selfDigits = targets.self.e164.replace(/\D/g, "");
|
||||
if (selfDigits) {
|
||||
const bodyDigits = bodyClean.replace(/[^\d]/g, "");
|
||||
if (bodyDigits.includes(selfDigits)) {
|
||||
@@ -94,14 +92,14 @@ export function debugMention(
|
||||
from: msg.from,
|
||||
body: msg.body,
|
||||
bodyClean: normalizeMentionText(msg.body),
|
||||
mentionedJids: msg.mentionedJids ?? null,
|
||||
mentionedJids: msg.mentions ?? msg.mentionedJids ?? null,
|
||||
normalizedMentionedJids: mentionTargets.normalizedMentions.length
|
||||
? mentionTargets.normalizedMentions
|
||||
? mentionTargets.normalizedMentions.map((identity) => getComparableIdentityValues(identity))
|
||||
: null,
|
||||
selfJid: msg.selfJid ?? null,
|
||||
selfJidBare: mentionTargets.selfJid,
|
||||
selfE164: msg.selfE164 ?? null,
|
||||
resolvedSelfE164: mentionTargets.selfE164,
|
||||
selfJid: msg.self?.jid ?? msg.selfJid ?? null,
|
||||
selfLid: msg.self?.lid ?? msg.selfLid ?? null,
|
||||
selfE164: msg.self?.e164 ?? msg.selfE164 ?? null,
|
||||
resolvedSelf: mentionTargets.self,
|
||||
};
|
||||
return { wasMentioned: result, details };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";
|
||||
import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { getSenderIdentity } from "../../identity.js";
|
||||
import { sendReactionWhatsApp } from "../../send.js";
|
||||
import { formatError } from "../../session.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
@@ -55,10 +56,11 @@ export function maybeSendAckReaction(params: {
|
||||
{ chatId: params.msg.chatId, messageId: params.msg.id, emoji },
|
||||
"sending ack reaction",
|
||||
);
|
||||
const sender = getSenderIdentity(params.msg);
|
||||
sendReactionWhatsApp(params.msg.chatId, params.msg.id, emoji, {
|
||||
verbose: params.verbose,
|
||||
fromMe: false,
|
||||
participant: params.msg.senderJid,
|
||||
participant: sender.jid ?? undefined,
|
||||
accountId: params.accountId,
|
||||
}).catch((err) => {
|
||||
params.warn(
|
||||
|
||||
@@ -4,6 +4,13 @@ import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
||||
import { parseActivationCommand } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
getPrimaryIdentityId,
|
||||
getReplyContext,
|
||||
getSelfIdentity,
|
||||
getSenderIdentity,
|
||||
identitiesOverlap,
|
||||
} from "../../identity.js";
|
||||
import type { MentionConfig } from "../mentions.js";
|
||||
import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
@@ -36,11 +43,11 @@ type ApplyGroupGatingParams = {
|
||||
};
|
||||
|
||||
function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMsg) {
|
||||
const sender = normalizeE164(msg.senderE164 ?? "");
|
||||
const sender = normalizeE164(getSenderIdentity(msg).e164 ?? "");
|
||||
if (!sender) {
|
||||
return false;
|
||||
}
|
||||
const owners = resolveOwnerList(baseMentionConfig, msg.selfE164 ?? undefined);
|
||||
const owners = resolveOwnerList(baseMentionConfig, getSelfIdentity(msg).e164 ?? undefined);
|
||||
return owners.includes(sender);
|
||||
}
|
||||
|
||||
@@ -50,10 +57,14 @@ function recordPendingGroupHistoryEntry(params: {
|
||||
groupHistoryKey: string;
|
||||
groupHistoryLimit: number;
|
||||
}) {
|
||||
const senderIdentity = getSenderIdentity(params.msg);
|
||||
const sender =
|
||||
params.msg.senderName && params.msg.senderE164
|
||||
? `${params.msg.senderName} (${params.msg.senderE164})`
|
||||
: (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown");
|
||||
senderIdentity.name && senderIdentity.e164
|
||||
? `${senderIdentity.name} (${senderIdentity.e164})`
|
||||
: (senderIdentity.name ??
|
||||
senderIdentity.e164 ??
|
||||
getPrimaryIdentityId(senderIdentity) ??
|
||||
"Unknown");
|
||||
recordPendingHistoryEntryIfEnabled({
|
||||
historyMap: params.groupHistories,
|
||||
historyKey: params.groupHistoryKey,
|
||||
@@ -63,7 +74,7 @@ function recordPendingGroupHistoryEntry(params: {
|
||||
body: params.msg.body,
|
||||
timestamp: params.msg.timestamp,
|
||||
id: params.msg.id,
|
||||
senderJid: params.msg.senderJid,
|
||||
senderJid: senderIdentity.jid ?? params.msg.senderJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -80,6 +91,8 @@ function skipGroupMessageAndStoreHistory(params: ApplyGroupGatingParams, verbose
|
||||
}
|
||||
|
||||
export function applyGroupGating(params: ApplyGroupGatingParams) {
|
||||
const sender = getSenderIdentity(params.msg);
|
||||
const self = getSelfIdentity(params.msg, params.authDir);
|
||||
const groupPolicy = resolveGroupPolicyFor(params.cfg, params.conversationId);
|
||||
if (groupPolicy.allowlistEnabled && !groupPolicy.allowed) {
|
||||
params.logVerbose(`Skipping group message ${params.conversationId} (not in allowlist)`);
|
||||
@@ -89,15 +102,15 @@ export function applyGroupGating(params: ApplyGroupGatingParams) {
|
||||
noteGroupMember(
|
||||
params.groupMemberNames,
|
||||
params.groupHistoryKey,
|
||||
params.msg.senderE164,
|
||||
params.msg.senderName,
|
||||
sender.e164 ?? undefined,
|
||||
sender.name ?? undefined,
|
||||
);
|
||||
|
||||
const mentionConfig = buildMentionConfig(params.cfg, params.agentId);
|
||||
const commandBody = stripMentionsForCommand(
|
||||
params.msg.body,
|
||||
mentionConfig.mentionRegexes,
|
||||
params.msg.selfE164,
|
||||
self.e164,
|
||||
);
|
||||
const activationCommand = parseActivationCommand(commandBody);
|
||||
const owner = isOwnerSender(params.baseMentionConfig, params.msg);
|
||||
@@ -127,21 +140,11 @@ export function applyGroupGating(params: ApplyGroupGatingParams) {
|
||||
conversationId: params.conversationId,
|
||||
});
|
||||
const requireMention = activation !== "always";
|
||||
const selfJid = params.msg.selfJid?.replace(/:\d+/, "");
|
||||
const selfLid = params.msg.selfLid?.replace(/:\d+/, "");
|
||||
const replySenderJid = params.msg.replyToSenderJid?.replace(/:\d+/, "");
|
||||
const selfE164 = params.msg.selfE164 ? normalizeE164(params.msg.selfE164) : null;
|
||||
const replySenderE164 = params.msg.replyToSenderE164
|
||||
? normalizeE164(params.msg.replyToSenderE164)
|
||||
: null;
|
||||
const replyContext = getReplyContext(params.msg, params.authDir);
|
||||
// Detect reply-to-bot: compare JIDs, LIDs, and E.164 numbers.
|
||||
// WhatsApp may report the quoted message sender as either a phone JID
|
||||
// (xxxxx@s.whatsapp.net) or a LID (xxxxx@lid), so we compare both.
|
||||
const implicitMention = Boolean(
|
||||
(selfJid && replySenderJid && selfJid === replySenderJid) ||
|
||||
(selfLid && replySenderJid && selfLid === replySenderJid) ||
|
||||
(selfE164 && replySenderE164 && selfE164 === replySenderE164),
|
||||
);
|
||||
const implicitMention = identitiesOverlap(self, replyContext?.sender);
|
||||
const mentionGate = resolveMentionGating({
|
||||
requireMention,
|
||||
canDetectMention: true,
|
||||
|
||||
@@ -4,15 +4,17 @@ import {
|
||||
type EnvelopeFormatOptions,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { getPrimaryIdentityId, getReplyContext, getSenderIdentity } from "../../identity.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
|
||||
export function formatReplyContext(msg: WebInboundMsg) {
|
||||
if (!msg.replyToBody) {
|
||||
const replyTo = getReplyContext(msg);
|
||||
if (!replyTo?.body) {
|
||||
return null;
|
||||
}
|
||||
const sender = msg.replyToSender ?? "unknown sender";
|
||||
const idPart = msg.replyToId ? ` id:${msg.replyToId}` : "";
|
||||
return `[Replying to ${sender}${idPart}]\n${msg.replyToBody}\n[/Replying]`;
|
||||
const sender = replyTo.sender?.label ?? replyTo.sender?.e164 ?? "unknown sender";
|
||||
const idPart = replyTo.id ? ` id:${replyTo.id}` : "";
|
||||
return `[Replying to ${sender}${idPart}]\n${replyTo.body}\n[/Replying]`;
|
||||
}
|
||||
|
||||
export function buildInboundLine(params: {
|
||||
@@ -31,6 +33,7 @@ export function buildInboundLine(params: {
|
||||
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
|
||||
const replyContext = formatReplyContext(msg);
|
||||
const baseLine = `${prefixStr}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
|
||||
const sender = getSenderIdentity(msg);
|
||||
|
||||
// Wrap with standardized envelope for the agent.
|
||||
return formatInboundEnvelope({
|
||||
@@ -40,9 +43,9 @@ export function buildInboundLine(params: {
|
||||
body: baseLine,
|
||||
chatType: msg.chatType,
|
||||
sender: {
|
||||
name: msg.senderName,
|
||||
e164: msg.senderE164,
|
||||
id: msg.senderJid,
|
||||
name: sender.name ?? undefined,
|
||||
e164: sender.e164 ?? undefined,
|
||||
id: getPrimaryIdentityId(sender) ?? undefined,
|
||||
},
|
||||
previousTimestamp,
|
||||
envelope,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { buildGroupHistoryKey } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { getPrimaryIdentityId, getSenderIdentity } from "../../identity.js";
|
||||
import type { MentionConfig } from "../mentions.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { maybeBroadcastMessage } from "./broadcast.js";
|
||||
@@ -96,6 +97,7 @@ export function createWebOnMessageHandler(params: {
|
||||
}
|
||||
|
||||
if (msg.chatType === "group") {
|
||||
const sender = getSenderIdentity(msg);
|
||||
const metaCtx = {
|
||||
From: msg.from,
|
||||
To: msg.to,
|
||||
@@ -104,9 +106,9 @@ export function createWebOnMessageHandler(params: {
|
||||
ChatType: msg.chatType,
|
||||
ConversationLabel: conversationId,
|
||||
GroupSubject: msg.groupSubject,
|
||||
SenderName: msg.senderName,
|
||||
SenderId: msg.senderJid?.trim() || msg.senderE164,
|
||||
SenderE164: msg.senderE164,
|
||||
SenderName: sender.name ?? undefined,
|
||||
SenderId: getPrimaryIdentityId(sender) ?? undefined,
|
||||
SenderE164: sender.e164 ?? undefined,
|
||||
Provider: "whatsapp",
|
||||
Surface: "whatsapp",
|
||||
OriginatingChannel: "whatsapp",
|
||||
@@ -144,8 +146,12 @@ export function createWebOnMessageHandler(params: {
|
||||
}
|
||||
} else {
|
||||
// Ensure `peerId` for DMs is stable and stored as E.164 when possible.
|
||||
if (!msg.senderE164 && peerId && peerId.startsWith("+")) {
|
||||
msg.senderE164 = normalizeE164(peerId) ?? msg.senderE164;
|
||||
if (!msg.sender?.e164 && !msg.senderE164 && peerId && peerId.startsWith("+")) {
|
||||
const normalized = normalizeE164(peerId);
|
||||
if (normalized) {
|
||||
msg.sender = { ...(msg.sender ?? {}), e164: normalized };
|
||||
msg.senderE164 = normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { jidToE164, normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { getSenderIdentity } from "../../identity.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
|
||||
export function resolvePeerId(msg: WebInboundMsg) {
|
||||
if (msg.chatType === "group") {
|
||||
return msg.conversationId ?? msg.from;
|
||||
}
|
||||
if (msg.senderE164) {
|
||||
return normalizeE164(msg.senderE164) ?? msg.senderE164;
|
||||
const sender = getSenderIdentity(msg);
|
||||
if (sender.e164) {
|
||||
return normalizeE164(sender.e164) ?? sender.e164;
|
||||
}
|
||||
if (msg.from.includes("@")) {
|
||||
return jidToE164(msg.from) ?? msg.from;
|
||||
|
||||
@@ -33,6 +33,12 @@ import {
|
||||
} from "openclaw/plugin-sdk/security-runtime";
|
||||
import { jidToE164, normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveWhatsAppAccount } from "../../accounts.js";
|
||||
import {
|
||||
getPrimaryIdentityId,
|
||||
getReplyContext,
|
||||
getSelfIdentity,
|
||||
getSenderIdentity,
|
||||
} from "../../identity.js";
|
||||
import { newConnectionId } from "../../reconnect.js";
|
||||
import { formatError } from "../../session.js";
|
||||
import { deliverWebReply } from "../deliver-reply.js";
|
||||
@@ -62,8 +68,10 @@ async function resolveWhatsAppCommandAuthorized(params: {
|
||||
}
|
||||
|
||||
const isGroup = params.msg.chatType === "group";
|
||||
const sender = getSenderIdentity(params.msg);
|
||||
const self = getSelfIdentity(params.msg);
|
||||
const senderE164 = normalizeE164(
|
||||
isGroup ? (params.msg.senderE164 ?? "") : (params.msg.senderE164 ?? params.msg.from ?? ""),
|
||||
isGroup ? (sender.e164 ?? "") : (sender.e164 ?? params.msg.from ?? ""),
|
||||
);
|
||||
if (!senderE164) {
|
||||
return false;
|
||||
@@ -84,11 +92,7 @@ async function resolveWhatsAppCommandAuthorized(params: {
|
||||
dmPolicy,
|
||||
});
|
||||
const dmAllowFrom =
|
||||
configuredAllowFrom.length > 0
|
||||
? configuredAllowFrom
|
||||
: params.msg.selfE164
|
||||
? [params.msg.selfE164]
|
||||
: [];
|
||||
configuredAllowFrom.length > 0 ? configuredAllowFrom : self.e164 ? [self.e164] : [];
|
||||
const access = resolveDmGroupAccessWithCommandGate({
|
||||
isGroup,
|
||||
dmPolicy,
|
||||
@@ -244,11 +248,14 @@ export async function processMessage(params: {
|
||||
whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
|
||||
}
|
||||
|
||||
const sender = getSenderIdentity(params.msg);
|
||||
const self = getSelfIdentity(params.msg);
|
||||
const replyTo = getReplyContext(params.msg);
|
||||
const dmRouteTarget =
|
||||
params.msg.chatType !== "group"
|
||||
? (() => {
|
||||
if (params.msg.senderE164) {
|
||||
return normalizeE164(params.msg.senderE164);
|
||||
if (sender.e164) {
|
||||
return normalizeE164(sender.e164);
|
||||
}
|
||||
// In direct chats, `msg.from` is already the canonical conversation id.
|
||||
if (params.msg.from.includes("@")) {
|
||||
@@ -280,8 +287,8 @@ export async function processMessage(params: {
|
||||
});
|
||||
const isSelfChat =
|
||||
params.msg.chatType !== "group" &&
|
||||
Boolean(params.msg.selfE164) &&
|
||||
normalizeE164(params.msg.from) === normalizeE164(params.msg.selfE164 ?? "");
|
||||
Boolean(self.e164) &&
|
||||
normalizeE164(params.msg.from) === normalizeE164(self.e164 ?? "");
|
||||
const responsePrefix =
|
||||
replyPipeline.responsePrefix ??
|
||||
(configuredResponsePrefix === undefined && isSelfChat
|
||||
@@ -310,9 +317,9 @@ export async function processMessage(params: {
|
||||
SessionKey: params.route.sessionKey,
|
||||
AccountId: params.route.accountId,
|
||||
MessageSid: params.msg.id,
|
||||
ReplyToId: params.msg.replyToId,
|
||||
ReplyToBody: params.msg.replyToBody,
|
||||
ReplyToSender: params.msg.replyToSender,
|
||||
ReplyToId: replyTo?.id,
|
||||
ReplyToBody: replyTo?.body,
|
||||
ReplyToSender: replyTo?.sender?.label,
|
||||
MediaPath: params.msg.mediaPath,
|
||||
MediaUrl: params.msg.mediaUrl,
|
||||
MediaType: params.msg.mediaType,
|
||||
@@ -322,11 +329,11 @@ export async function processMessage(params: {
|
||||
GroupMembers: formatGroupMembers({
|
||||
participants: params.msg.groupParticipants,
|
||||
roster: params.groupMemberNames.get(params.groupHistoryKey),
|
||||
fallbackE164: params.msg.senderE164,
|
||||
fallbackE164: sender.e164 ?? undefined,
|
||||
}),
|
||||
SenderName: params.msg.senderName,
|
||||
SenderId: params.msg.senderJid?.trim() || params.msg.senderE164,
|
||||
SenderE164: params.msg.senderE164,
|
||||
SenderName: sender.name ?? undefined,
|
||||
SenderId: getPrimaryIdentityId(sender) ?? undefined,
|
||||
SenderE164: sender.e164 ?? undefined,
|
||||
CommandAuthorized: commandAuthorized,
|
||||
WasMentioned: params.msg.wasMentioned,
|
||||
...(params.msg.location ? toLocationContext(params.msg.location) : {}),
|
||||
|
||||
@@ -115,7 +115,13 @@ describe("resolveMentionTargets with @lid mapping", () => {
|
||||
}),
|
||||
authDir,
|
||||
);
|
||||
expect(mentionTargets.normalizedMentions).toContain("+1777");
|
||||
expect(mentionTargets.normalizedMentions).toEqual([
|
||||
expect.objectContaining({
|
||||
jid: null,
|
||||
lid: "777@lid",
|
||||
e164: "+1777",
|
||||
}),
|
||||
]);
|
||||
|
||||
const selfTargets = resolveMentionTargets(
|
||||
makeMsg({
|
||||
@@ -124,7 +130,8 @@ describe("resolveMentionTargets with @lid mapping", () => {
|
||||
}),
|
||||
authDir,
|
||||
);
|
||||
expect(selfTargets.selfE164).toBe("+1777");
|
||||
expect(selfTargets.self.e164).toBe("+1777");
|
||||
expect(selfTargets.self.lid).toBe("777@lid");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
164
extensions/whatsapp/src/identity.ts
Normal file
164
extensions/whatsapp/src/identity.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { jidToE164, normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
const WHATSAPP_LID_RE = /@(lid|hosted\.lid)$/i;
|
||||
|
||||
export type WhatsAppIdentity = {
|
||||
jid?: string | null;
|
||||
lid?: string | null;
|
||||
e164?: string | null;
|
||||
name?: string | null;
|
||||
label?: string | null;
|
||||
};
|
||||
|
||||
export type WhatsAppSelfIdentity = {
|
||||
jid?: string | null;
|
||||
lid?: string | null;
|
||||
e164?: string | null;
|
||||
};
|
||||
|
||||
export type WhatsAppReplyContext = {
|
||||
id?: string;
|
||||
body: string;
|
||||
sender?: WhatsAppIdentity | null;
|
||||
};
|
||||
|
||||
type LegacySenderLike = {
|
||||
sender?: WhatsAppIdentity;
|
||||
senderJid?: string;
|
||||
senderE164?: string;
|
||||
senderName?: string;
|
||||
};
|
||||
|
||||
type LegacySelfLike = {
|
||||
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;
|
||||
};
|
||||
|
||||
type LegacyMentionsLike = {
|
||||
mentions?: string[];
|
||||
mentionedJids?: string[];
|
||||
};
|
||||
|
||||
export function normalizeDeviceScopedJid(jid: string | null | undefined): string | null {
|
||||
return jid ? jid.replace(/:\d+/, "") : null;
|
||||
}
|
||||
|
||||
function isLidJid(jid: string | null | undefined): boolean {
|
||||
return Boolean(jid && WHATSAPP_LID_RE.test(jid));
|
||||
}
|
||||
|
||||
export function resolveComparableIdentity(
|
||||
identity: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
|
||||
authDir?: string,
|
||||
): WhatsAppIdentity {
|
||||
const rawJid = normalizeDeviceScopedJid(identity?.jid);
|
||||
const rawLid = normalizeDeviceScopedJid(identity?.lid);
|
||||
const lid = rawLid ?? (isLidJid(rawJid) ? rawJid : null);
|
||||
const jid = rawJid && !isLidJid(rawJid) ? rawJid : null;
|
||||
const e164 =
|
||||
identity?.e164 != null
|
||||
? normalizeE164(identity.e164)
|
||||
: ((jid ? jidToE164(jid, authDir ? { authDir } : undefined) : null) ??
|
||||
(lid ? jidToE164(lid, authDir ? { authDir } : undefined) : null));
|
||||
return {
|
||||
...identity,
|
||||
jid,
|
||||
lid,
|
||||
e164,
|
||||
};
|
||||
}
|
||||
|
||||
export function getComparableIdentityValues(
|
||||
identity: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
|
||||
): string[] {
|
||||
const resolved = resolveComparableIdentity(identity);
|
||||
return [resolved.e164, resolved.jid, resolved.lid].filter((value): value is string =>
|
||||
Boolean(value),
|
||||
);
|
||||
}
|
||||
|
||||
export function identitiesOverlap(
|
||||
left: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
|
||||
right: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
|
||||
): boolean {
|
||||
const leftValues = new Set(getComparableIdentityValues(left));
|
||||
if (leftValues.size === 0) {
|
||||
return false;
|
||||
}
|
||||
return getComparableIdentityValues(right).some((value) => leftValues.has(value));
|
||||
}
|
||||
|
||||
export function getSenderIdentity(msg: LegacySenderLike, authDir?: string): WhatsAppIdentity {
|
||||
return resolveComparableIdentity(
|
||||
msg.sender ?? {
|
||||
jid: msg.senderJid ?? null,
|
||||
e164: msg.senderE164 ?? null,
|
||||
name: msg.senderName ?? null,
|
||||
},
|
||||
authDir,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSelfIdentity(msg: LegacySelfLike, authDir?: string): WhatsAppSelfIdentity {
|
||||
return resolveComparableIdentity(
|
||||
msg.self ?? {
|
||||
jid: msg.selfJid ?? null,
|
||||
lid: msg.selfLid ?? null,
|
||||
e164: msg.selfE164 ?? null,
|
||||
},
|
||||
authDir,
|
||||
);
|
||||
}
|
||||
|
||||
export function getReplyContext(
|
||||
msg: LegacyReplyLike,
|
||||
authDir?: string,
|
||||
): WhatsAppReplyContext | null {
|
||||
if (msg.replyTo) {
|
||||
return {
|
||||
...msg.replyTo,
|
||||
sender: resolveComparableIdentity(msg.replyTo.sender, authDir),
|
||||
};
|
||||
}
|
||||
if (!msg.replyToBody) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: msg.replyToId,
|
||||
body: msg.replyToBody,
|
||||
sender: resolveComparableIdentity(
|
||||
{
|
||||
jid: msg.replyToSenderJid ?? null,
|
||||
e164: msg.replyToSenderE164 ?? null,
|
||||
label: msg.replyToSender ?? null,
|
||||
},
|
||||
authDir,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function getMentionJids(msg: LegacyMentionsLike): string[] {
|
||||
return msg.mentions ?? msg.mentionedJids ?? [];
|
||||
}
|
||||
|
||||
export function getMentionIdentities(
|
||||
msg: LegacyMentionsLike,
|
||||
authDir?: string,
|
||||
): WhatsAppIdentity[] {
|
||||
return getMentionJids(msg).map((jid) => resolveComparableIdentity({ jid }, authDir));
|
||||
}
|
||||
|
||||
export function getPrimaryIdentityId(identity: WhatsAppIdentity | null | undefined): string | null {
|
||||
return identity?.e164 || identity?.jid?.trim() || identity?.lid || null;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { formatLocationText, type NormalizedLocation } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { jidToE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveComparableIdentity, type WhatsAppReplyContext } from "../identity.js";
|
||||
import { parseVcard } from "../vcard.js";
|
||||
|
||||
const MESSAGE_WRAPPER_KEYS = [
|
||||
@@ -107,46 +108,43 @@ function extractMessage(message: proto.IMessage | undefined): proto.IMessage | u
|
||||
return candidate && typeof candidate === "object" ? (candidate as proto.IMessage) : normalized;
|
||||
}
|
||||
|
||||
function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | undefined {
|
||||
let normalized = normalizeMessage(message);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
// Generic FutureProofMessage unwrap for wrappers that Baileys'
|
||||
// normalizeMessageContent doesn't handle yet (e.g. botInvokeMessage,
|
||||
// groupMentionedMessage). These wrappers have shape { message: IMessage }.
|
||||
// Iterate up to 3 times to handle nested wrappers.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const contentType = getMessageContentType(normalized);
|
||||
if (!contentType) {
|
||||
break;
|
||||
}
|
||||
const value = (normalized as Record<string, unknown>)[contentType];
|
||||
if (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
"message" in value &&
|
||||
(value as { message?: unknown }).message &&
|
||||
typeof (value as { message: unknown }).message === "object"
|
||||
) {
|
||||
const inner = normalizeMessage((value as { message: proto.IMessage }).message);
|
||||
if (inner) {
|
||||
const innerType = getMessageContentType(inner);
|
||||
if (innerType && innerType !== contentType) {
|
||||
normalized = inner;
|
||||
continue;
|
||||
}
|
||||
function getFutureProofInnerMessage(message: proto.IMessage): proto.IMessage | undefined {
|
||||
const contentType = getMessageContentType(message);
|
||||
const candidate = contentType ? (message as Record<string, unknown>)[contentType] : undefined;
|
||||
if (
|
||||
candidate &&
|
||||
typeof candidate === "object" &&
|
||||
"message" in candidate &&
|
||||
(candidate as { message?: unknown }).message &&
|
||||
typeof (candidate as { message: unknown }).message === "object"
|
||||
) {
|
||||
const inner = normalizeMessage((candidate as { message: proto.IMessage }).message);
|
||||
if (inner) {
|
||||
const innerType = getMessageContentType(inner);
|
||||
if (innerType && innerType !== contentType) {
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return normalized;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractContextInfo(message: proto.IMessage | undefined): proto.IContextInfo | undefined {
|
||||
if (!message) {
|
||||
return undefined;
|
||||
function buildMessageChain(message: proto.IMessage | undefined): proto.IMessage[] {
|
||||
const chain: proto.IMessage[] = [];
|
||||
let current = normalizeMessage(message);
|
||||
while (current && chain.length < 4) {
|
||||
chain.push(current);
|
||||
current = getFutureProofInnerMessage(current);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | undefined {
|
||||
const chain = buildMessageChain(message);
|
||||
return chain.at(-1);
|
||||
}
|
||||
|
||||
function extractContextInfoFromMessage(message: proto.IMessage): proto.IContextInfo | undefined {
|
||||
const contentType = getMessageContentType(message);
|
||||
const candidate = contentType ? (message as Record<string, unknown>)[contentType] : undefined;
|
||||
const contextInfo =
|
||||
@@ -196,6 +194,16 @@ function extractContextInfo(message: proto.IMessage | undefined): proto.IContext
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractContextInfo(message: proto.IMessage | undefined): proto.IContextInfo | undefined {
|
||||
for (const candidate of buildMessageChain(message)) {
|
||||
const contextInfo = extractContextInfoFromMessage(candidate);
|
||||
if (contextInfo) {
|
||||
return contextInfo;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function extractMentionedJids(rawMessage: proto.IMessage | undefined): string[] | undefined {
|
||||
const message = unwrapMessage(rawMessage);
|
||||
if (!message) {
|
||||
@@ -426,13 +434,9 @@ export function extractLocationData(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function describeReplyContext(rawMessage: proto.IMessage | undefined): {
|
||||
id?: string;
|
||||
body: string;
|
||||
sender: string;
|
||||
senderJid?: string;
|
||||
senderE164?: string;
|
||||
} | null {
|
||||
export function describeReplyContext(
|
||||
rawMessage: proto.IMessage | undefined,
|
||||
): WhatsAppReplyContext | null {
|
||||
const message = unwrapMessage(rawMessage);
|
||||
if (!message) {
|
||||
return null;
|
||||
@@ -457,13 +461,13 @@ export function describeReplyContext(rawMessage: proto.IMessage | undefined): {
|
||||
return null;
|
||||
}
|
||||
const senderJid = contextInfo?.participant ?? undefined;
|
||||
const senderE164 = senderJid ? (jidToE164(senderJid) ?? senderJid) : undefined;
|
||||
const sender = senderE164 ?? "unknown sender";
|
||||
const sender = resolveComparableIdentity({
|
||||
jid: senderJid,
|
||||
label: senderJid ? (jidToE164(senderJid) ?? senderJid) : "unknown sender",
|
||||
});
|
||||
return {
|
||||
id: contextInfo?.stanzaId ? String(contextInfo.stanzaId) : undefined,
|
||||
body,
|
||||
sender,
|
||||
senderJid,
|
||||
senderE164,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { AnyMessageContent, proto, WAMessage } from "@whiskeysockets/baileys";
|
||||
import { DisconnectReason, isJidGroup } from "@whiskeysockets/baileys";
|
||||
import { createInboundDebouncer, formatLocationText } from "openclaw/plugin-sdk/channel-inbound";
|
||||
@@ -8,7 +6,9 @@ import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { getChildLogger } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { jidToE164, resolveJidToE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveJidToE164 } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { readWebSelfIdentity } from "../auth-store.js";
|
||||
import { getPrimaryIdentityId, resolveComparableIdentity } from "../identity.js";
|
||||
import { createWaSocket, getStatusCode, waitForWaConnection } from "../session.js";
|
||||
import { checkInboundAccessControl } from "./access-control.js";
|
||||
import {
|
||||
@@ -77,27 +77,21 @@ export async function monitorWebInbox(options: {
|
||||
logVerbose(`Failed to send 'available' presence on connect: ${String(err)}`);
|
||||
}
|
||||
|
||||
const selfJid = sock.user?.id;
|
||||
const selfE164 = selfJid ? jidToE164(selfJid) : null;
|
||||
// Bot's own LID (Linked Identity) — needed for reply-to-bot detection
|
||||
// when contextInfo.participant returns a LID instead of a phone JID.
|
||||
// Baileys 7 rc9 does not expose lid on sock.user, so read from creds file.
|
||||
const selfLid = await (async () => {
|
||||
try {
|
||||
const credsPath = path.join(options.authDir, "creds.json");
|
||||
const raw = await fs.promises.readFile(credsPath, "utf-8");
|
||||
const creds = JSON.parse(raw) as { me?: { lid?: string } };
|
||||
return creds?.me?.lid ?? undefined;
|
||||
} catch {
|
||||
return (sock.user as { lid?: string } | undefined)?.lid ?? undefined;
|
||||
}
|
||||
})();
|
||||
const self = await readWebSelfIdentity(
|
||||
options.authDir,
|
||||
sock.user as { id?: string | null; lid?: string | null } | undefined,
|
||||
);
|
||||
const debouncer = createInboundDebouncer<WebInboundMessage>({
|
||||
debounceMs: options.debounceMs ?? 0,
|
||||
buildKey: (msg) => {
|
||||
const sender = msg.sender;
|
||||
const senderKey =
|
||||
msg.chatType === "group"
|
||||
? (msg.senderJid ?? msg.senderE164 ?? msg.senderName ?? msg.from)
|
||||
? (getPrimaryIdentityId(sender ?? null) ??
|
||||
msg.senderJid ??
|
||||
msg.senderE164 ??
|
||||
msg.senderName ??
|
||||
msg.from)
|
||||
: msg.from;
|
||||
if (!senderKey) {
|
||||
return null;
|
||||
@@ -117,7 +111,7 @@ export async function monitorWebInbox(options: {
|
||||
}
|
||||
const mentioned = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
for (const jid of entry.mentionedJids ?? []) {
|
||||
for (const jid of entry.mentions ?? entry.mentionedJids ?? []) {
|
||||
mentioned.add(jid);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +122,7 @@ export async function monitorWebInbox(options: {
|
||||
const combinedMessage: WebInboundMessage = {
|
||||
...last,
|
||||
body: combinedBody,
|
||||
mentions: mentioned.size > 0 ? Array.from(mentioned) : undefined,
|
||||
mentionedJids: mentioned.size > 0 ? Array.from(mentioned) : undefined,
|
||||
};
|
||||
await options.onMessage(combinedMessage);
|
||||
@@ -267,7 +262,7 @@ export async function monitorWebInbox(options: {
|
||||
const access = await checkInboundAccessControl({
|
||||
accountId: options.accountId,
|
||||
from,
|
||||
selfE164,
|
||||
selfE164: self.e164 ?? null,
|
||||
senderE164,
|
||||
group,
|
||||
pushName: msg.pushName ?? undefined,
|
||||
@@ -399,7 +394,7 @@ export async function monitorWebInbox(options: {
|
||||
inboundLogger.info(
|
||||
{
|
||||
from: inbound.from,
|
||||
to: selfE164 ?? "me",
|
||||
to: self.e164 ?? "me",
|
||||
body: enriched.body,
|
||||
mediaPath: enriched.mediaPath,
|
||||
mediaType: enriched.mediaType,
|
||||
@@ -412,27 +407,35 @@ export async function monitorWebInbox(options: {
|
||||
id: inbound.id,
|
||||
from: inbound.from,
|
||||
conversationId: inbound.from,
|
||||
to: selfE164 ?? "me",
|
||||
to: self.e164 ?? "me",
|
||||
accountId: inbound.access.resolvedAccountId,
|
||||
body: enriched.body,
|
||||
pushName: senderName,
|
||||
timestamp,
|
||||
chatType: inbound.group ? "group" : "direct",
|
||||
chatId: inbound.remoteJid,
|
||||
sender: resolveComparableIdentity({
|
||||
jid: inbound.participantJid,
|
||||
e164: inbound.senderE164 ?? undefined,
|
||||
name: senderName,
|
||||
}),
|
||||
senderJid: inbound.participantJid,
|
||||
senderE164: inbound.senderE164 ?? undefined,
|
||||
senderName,
|
||||
replyTo: enriched.replyContext ?? undefined,
|
||||
replyToId: enriched.replyContext?.id,
|
||||
replyToBody: enriched.replyContext?.body,
|
||||
replyToSender: enriched.replyContext?.sender,
|
||||
replyToSenderJid: enriched.replyContext?.senderJid,
|
||||
replyToSenderE164: enriched.replyContext?.senderE164,
|
||||
replyToSender: enriched.replyContext?.sender?.label ?? undefined,
|
||||
replyToSenderJid: enriched.replyContext?.sender?.jid ?? undefined,
|
||||
replyToSenderE164: enriched.replyContext?.sender?.e164 ?? undefined,
|
||||
groupSubject: inbound.groupSubject,
|
||||
groupParticipants: inbound.groupParticipants,
|
||||
mentions: mentionedJids ?? undefined,
|
||||
mentionedJids: mentionedJids ?? undefined,
|
||||
selfJid,
|
||||
selfLid,
|
||||
selfE164,
|
||||
self,
|
||||
selfJid: self.jid ?? undefined,
|
||||
selfLid: self.lid ?? undefined,
|
||||
selfE164: self.e164 ?? undefined,
|
||||
fromMe: Boolean(msg.key?.fromMe),
|
||||
location: enriched.location ?? undefined,
|
||||
sendComposing,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AnyMessageContent } from "@whiskeysockets/baileys";
|
||||
import type { NormalizedLocation } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { WhatsAppIdentity, WhatsAppReplyContext, WhatsAppSelfIdentity } from "../identity.js";
|
||||
|
||||
export type WebListenerCloseReason = {
|
||||
status?: number;
|
||||
@@ -18,9 +19,11 @@ export type WebInboundMessage = {
|
||||
timestamp?: number;
|
||||
chatType: "direct" | "group";
|
||||
chatId: string;
|
||||
sender?: WhatsAppIdentity;
|
||||
senderJid?: string;
|
||||
senderE164?: string;
|
||||
senderName?: string;
|
||||
replyTo?: WhatsAppReplyContext;
|
||||
replyToId?: string;
|
||||
replyToBody?: string;
|
||||
replyToSender?: string;
|
||||
@@ -28,7 +31,9 @@ export type WebInboundMessage = {
|
||||
replyToSenderE164?: string;
|
||||
groupSubject?: string;
|
||||
groupParticipants?: string[];
|
||||
mentions?: string[];
|
||||
mentionedJids?: string[];
|
||||
self?: WhatsAppSelfIdentity;
|
||||
selfJid?: string | null;
|
||||
selfLid?: string | null;
|
||||
selfE164?: string | null;
|
||||
|
||||
@@ -60,6 +60,23 @@ describe("web monitor inbox", () => {
|
||||
replyToId: "q1",
|
||||
replyToBody: "original",
|
||||
replyToSender: "+111",
|
||||
sender: expect.objectContaining({
|
||||
e164: "+999",
|
||||
name: "Tester",
|
||||
}),
|
||||
replyTo: expect.objectContaining({
|
||||
id: "q1",
|
||||
body: "original",
|
||||
sender: expect.objectContaining({
|
||||
jid: "111@s.whatsapp.net",
|
||||
e164: "+111",
|
||||
label: "+111",
|
||||
}),
|
||||
}),
|
||||
self: expect.objectContaining({
|
||||
jid: "123@s.whatsapp.net",
|
||||
e164: "+123",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", {
|
||||
@@ -268,4 +285,20 @@ describe("web monitor inbox", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("captures reply context from botInvokeMessage wrapped quoted messages", async () => {
|
||||
await expectQuotedReplyContext({
|
||||
botInvokeMessage: {
|
||||
message: { conversation: "original" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("captures reply context from groupMentionedMessage wrapped quoted messages", async () => {
|
||||
await expectQuotedReplyContext({
|
||||
groupMentionedMessage: {
|
||||
message: { conversation: "original" },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,6 +140,36 @@ describe("web session", () => {
|
||||
readSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logWebSelfId prints cached lid details when creds include a lid", () => {
|
||||
const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => {
|
||||
if (typeof p !== "string") {
|
||||
return false;
|
||||
}
|
||||
return p.endsWith("creds.json");
|
||||
});
|
||||
const readSpy = vi.spyOn(fsSync, "readFileSync").mockImplementation((p) => {
|
||||
if (typeof p === "string" && p.endsWith("creds.json")) {
|
||||
return JSON.stringify({
|
||||
me: { id: "12345@s.whatsapp.net", lid: "777@lid" },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected readFileSync path: ${String(p)}`);
|
||||
});
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
logWebSelfId("/tmp/wa-creds", runtime as never, true);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Web Channel: +12345 (jid 12345@s.whatsapp.net, lid 777@lid)"),
|
||||
);
|
||||
existsSpy.mockRestore();
|
||||
readSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("formatError prints Boom-like payload message", () => {
|
||||
const err = {
|
||||
error: {
|
||||
|
||||
Reference in New Issue
Block a user