Skip to content

Commit 3b6d980

Browse files
committed
refactor: unify whatsapp identity handling
1 parent cdba1e6 commit 3b6d980

15 files changed

Lines changed: 476 additions & 166 deletions

extensions/whatsapp/src/auth-store.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
88
import { defaultRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
99
import { resolveOAuthDir } from "openclaw/plugin-sdk/state-paths";
1010
import type { WebChannel } from "openclaw/plugin-sdk/text-runtime";
11-
import { jidToE164, resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
11+
import { resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
12+
import { resolveComparableIdentity, type WhatsAppSelfIdentity } from "./identity.js";
1213

1314
export function resolveDefaultWebAuthDir(): string {
1415
return path.join(resolveOAuthDir(), "whatsapp", DEFAULT_ACCOUNT_ID);
@@ -154,15 +155,51 @@ export function readWebSelfId(authDir: string = resolveDefaultWebAuthDir()) {
154155
try {
155156
const credsPath = resolveWebCredsPath(resolveUserPath(authDir));
156157
if (!fsSync.existsSync(credsPath)) {
157-
return { e164: null, jid: null } as const;
158+
return { e164: null, jid: null, lid: null } as const;
158159
}
159160
const raw = fsSync.readFileSync(credsPath, "utf-8");
160-
const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined;
161-
const jid = parsed?.me?.id ?? null;
162-
const e164 = jid ? jidToE164(jid, { authDir }) : null;
163-
return { e164, jid } as const;
161+
const parsed = JSON.parse(raw) as { me?: { id?: string; lid?: string } } | undefined;
162+
const identity = resolveComparableIdentity(
163+
{
164+
jid: parsed?.me?.id ?? null,
165+
lid: parsed?.me?.lid ?? null,
166+
},
167+
authDir,
168+
);
169+
return {
170+
e164: identity.e164 ?? null,
171+
jid: identity.jid ?? null,
172+
lid: identity.lid ?? null,
173+
} as const;
174+
} catch {
175+
return { e164: null, jid: null, lid: null } as const;
176+
}
177+
}
178+
179+
export async function readWebSelfIdentity(
180+
authDir: string = resolveDefaultWebAuthDir(),
181+
fallback?: { id?: string | null; lid?: string | null } | null,
182+
): Promise<WhatsAppSelfIdentity> {
183+
const resolvedAuthDir = resolveUserPath(authDir);
184+
maybeRestoreCredsFromBackup(resolvedAuthDir);
185+
try {
186+
const raw = await fs.readFile(resolveWebCredsPath(resolvedAuthDir), "utf-8");
187+
const parsed = JSON.parse(raw) as { me?: { id?: string; lid?: string } } | undefined;
188+
return resolveComparableIdentity(
189+
{
190+
jid: parsed?.me?.id ?? null,
191+
lid: parsed?.me?.lid ?? null,
192+
},
193+
resolvedAuthDir,
194+
);
164195
} catch {
165-
return { e164: null, jid: null } as const;
196+
return resolveComparableIdentity(
197+
{
198+
jid: fallback?.id ?? null,
199+
lid: fallback?.lid ?? null,
200+
},
201+
resolvedAuthDir,
202+
);
166203
}
167204
}
168205

@@ -185,8 +222,14 @@ export function logWebSelfId(
185222
includeChannelPrefix = false,
186223
) {
187224
// Human-friendly log of the currently linked personal web session.
188-
const { e164, jid } = readWebSelfId(authDir);
189-
const details = e164 || jid ? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}` : "unknown";
225+
const { e164, jid, lid } = readWebSelfId(authDir);
226+
const parts = [jid ? `jid ${jid}` : null, lid ? `lid ${lid}` : null].filter(
227+
(value): value is string => Boolean(value),
228+
);
229+
const details =
230+
e164 || parts.length > 0
231+
? `${e164 ?? "unknown"}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}`
232+
: "unknown";
190233
const prefix = includeChannelPrefix ? "Web Channel: " : "";
191234
runtime.log(info(`${prefix}${details}`));
192235
}

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

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { buildMentionRegexes, normalizeMentionText } from "openclaw/plugin-sdk/channel-inbound";
22
import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
3-
import { isSelfChatMode, jidToE164, normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
3+
import { isSelfChatMode, normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
4+
import {
5+
getComparableIdentityValues,
6+
getMentionIdentities,
7+
getSelfIdentity,
8+
identitiesOverlap,
9+
type WhatsAppIdentity,
10+
} from "../identity.js";
411
import type { WebInboundMsg } from "./types.js";
512

613
export type MentionConfig = {
@@ -9,9 +16,8 @@ export type MentionConfig = {
916
};
1017

1118
export type MentionTargets = {
12-
normalizedMentions: string[];
13-
selfE164: string | null;
14-
selfJid: string | null;
19+
normalizedMentions: WhatsAppIdentity[];
20+
self: WhatsAppIdentity;
1521
};
1622

1723
export function buildMentionConfig(
@@ -23,13 +29,9 @@ export function buildMentionConfig(
2329
}
2430

2531
export function resolveMentionTargets(msg: WebInboundMsg, authDir?: string): MentionTargets {
26-
const jidOptions = authDir ? { authDir } : undefined;
27-
const normalizedMentions = msg.mentionedJids?.length
28-
? msg.mentionedJids.map((jid) => jidToE164(jid, jidOptions) ?? jid).filter(Boolean)
29-
: [];
30-
const selfE164 = msg.selfE164 ?? (msg.selfJid ? jidToE164(msg.selfJid, jidOptions) : null);
31-
const selfJid = msg.selfJid ? msg.selfJid.replace(/:\\d+/, "") : null;
32-
return { normalizedMentions, selfE164, selfJid };
32+
const normalizedMentions = getMentionIdentities(msg, authDir);
33+
const self = getSelfIdentity(msg, authDir);
34+
return { normalizedMentions, self };
3335
}
3436

3537
export function isBotMentionedFromTargets(
@@ -41,16 +43,12 @@ export function isBotMentionedFromTargets(
4143
// Remove zero-width and directionality markers WhatsApp injects around display names
4244
normalizeMentionText(text);
4345

44-
const isSelfChat = isSelfChatMode(targets.selfE164, mentionCfg.allowFrom);
46+
const isSelfChat = isSelfChatMode(targets.self.e164, mentionCfg.allowFrom);
4547

46-
const hasMentions = (msg.mentionedJids?.length ?? 0) > 0;
48+
const hasMentions = targets.normalizedMentions.length > 0;
4749
if (hasMentions && !isSelfChat) {
48-
if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) {
49-
return true;
50-
}
51-
if (targets.selfJid) {
52-
// Some mentions use the bare JID; match on E.164 to be safe.
53-
if (targets.normalizedMentions.includes(targets.selfJid)) {
50+
for (const mention of targets.normalizedMentions) {
51+
if (identitiesOverlap(targets.self, mention)) {
5452
return true;
5553
}
5654
}
@@ -65,8 +63,8 @@ export function isBotMentionedFromTargets(
6563
}
6664

6765
// Fallback: detect body containing our own number (with or without +, spacing)
68-
if (targets.selfE164) {
69-
const selfDigits = targets.selfE164.replace(/\D/g, "");
66+
if (targets.self.e164) {
67+
const selfDigits = targets.self.e164.replace(/\D/g, "");
7068
if (selfDigits) {
7169
const bodyDigits = bodyClean.replace(/[^\d]/g, "");
7270
if (bodyDigits.includes(selfDigits)) {
@@ -94,14 +92,14 @@ export function debugMention(
9492
from: msg.from,
9593
body: msg.body,
9694
bodyClean: normalizeMentionText(msg.body),
97-
mentionedJids: msg.mentionedJids ?? null,
95+
mentionedJids: msg.mentions ?? msg.mentionedJids ?? null,
9896
normalizedMentionedJids: mentionTargets.normalizedMentions.length
99-
? mentionTargets.normalizedMentions
97+
? mentionTargets.normalizedMentions.map((identity) => getComparableIdentityValues(identity))
10098
: null,
101-
selfJid: msg.selfJid ?? null,
102-
selfJidBare: mentionTargets.selfJid,
103-
selfE164: msg.selfE164 ?? null,
104-
resolvedSelfE164: mentionTargets.selfE164,
99+
selfJid: msg.self?.jid ?? msg.selfJid ?? null,
100+
selfLid: msg.self?.lid ?? msg.selfLid ?? null,
101+
selfE164: msg.self?.e164 ?? msg.selfE164 ?? null,
102+
resolvedSelf: mentionTargets.self,
105103
};
106104
return { wasMentioned: result, details };
107105
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";
22
import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
33
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
4+
import { getSenderIdentity } from "../../identity.js";
45
import { sendReactionWhatsApp } from "../../send.js";
56
import { formatError } from "../../session.js";
67
import type { WebInboundMsg } from "../types.js";
@@ -55,10 +56,11 @@ export function maybeSendAckReaction(params: {
5556
{ chatId: params.msg.chatId, messageId: params.msg.id, emoji },
5657
"sending ack reaction",
5758
);
59+
const sender = getSenderIdentity(params.msg);
5860
sendReactionWhatsApp(params.msg.chatId, params.msg.id, emoji, {
5961
verbose: params.verbose,
6062
fromMe: false,
61-
participant: params.msg.senderJid,
63+
participant: sender.jid ?? undefined,
6264
accountId: params.accountId,
6365
}).catch((err) => {
6466
params.warn(

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

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
44
import { recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
55
import { parseActivationCommand } from "openclaw/plugin-sdk/reply-runtime";
66
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
7+
import {
8+
getPrimaryIdentityId,
9+
getReplyContext,
10+
getSelfIdentity,
11+
getSenderIdentity,
12+
identitiesOverlap,
13+
} from "../../identity.js";
714
import type { MentionConfig } from "../mentions.js";
815
import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js";
916
import type { WebInboundMsg } from "../types.js";
@@ -36,11 +43,11 @@ type ApplyGroupGatingParams = {
3643
};
3744

3845
function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMsg) {
39-
const sender = normalizeE164(msg.senderE164 ?? "");
46+
const sender = normalizeE164(getSenderIdentity(msg).e164 ?? "");
4047
if (!sender) {
4148
return false;
4249
}
43-
const owners = resolveOwnerList(baseMentionConfig, msg.selfE164 ?? undefined);
50+
const owners = resolveOwnerList(baseMentionConfig, getSelfIdentity(msg).e164 ?? undefined);
4451
return owners.includes(sender);
4552
}
4653

@@ -50,10 +57,14 @@ function recordPendingGroupHistoryEntry(params: {
5057
groupHistoryKey: string;
5158
groupHistoryLimit: number;
5259
}) {
60+
const senderIdentity = getSenderIdentity(params.msg);
5361
const sender =
54-
params.msg.senderName && params.msg.senderE164
55-
? `${params.msg.senderName} (${params.msg.senderE164})`
56-
: (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown");
62+
senderIdentity.name && senderIdentity.e164
63+
? `${senderIdentity.name} (${senderIdentity.e164})`
64+
: (senderIdentity.name ??
65+
senderIdentity.e164 ??
66+
getPrimaryIdentityId(senderIdentity) ??
67+
"Unknown");
5768
recordPendingHistoryEntryIfEnabled({
5869
historyMap: params.groupHistories,
5970
historyKey: params.groupHistoryKey,
@@ -63,7 +74,7 @@ function recordPendingGroupHistoryEntry(params: {
6374
body: params.msg.body,
6475
timestamp: params.msg.timestamp,
6576
id: params.msg.id,
66-
senderJid: params.msg.senderJid,
77+
senderJid: senderIdentity.jid ?? params.msg.senderJid,
6778
},
6879
});
6980
}
@@ -80,6 +91,8 @@ function skipGroupMessageAndStoreHistory(params: ApplyGroupGatingParams, verbose
8091
}
8192

8293
export function applyGroupGating(params: ApplyGroupGatingParams) {
94+
const sender = getSenderIdentity(params.msg);
95+
const self = getSelfIdentity(params.msg, params.authDir);
8396
const groupPolicy = resolveGroupPolicyFor(params.cfg, params.conversationId);
8497
if (groupPolicy.allowlistEnabled && !groupPolicy.allowed) {
8598
params.logVerbose(`Skipping group message ${params.conversationId} (not in allowlist)`);
@@ -89,15 +102,15 @@ export function applyGroupGating(params: ApplyGroupGatingParams) {
89102
noteGroupMember(
90103
params.groupMemberNames,
91104
params.groupHistoryKey,
92-
params.msg.senderE164,
93-
params.msg.senderName,
105+
sender.e164 ?? undefined,
106+
sender.name ?? undefined,
94107
);
95108

96109
const mentionConfig = buildMentionConfig(params.cfg, params.agentId);
97110
const commandBody = stripMentionsForCommand(
98111
params.msg.body,
99112
mentionConfig.mentionRegexes,
100-
params.msg.selfE164,
113+
self.e164,
101114
);
102115
const activationCommand = parseActivationCommand(commandBody);
103116
const owner = isOwnerSender(params.baseMentionConfig, params.msg);
@@ -127,21 +140,11 @@ export function applyGroupGating(params: ApplyGroupGatingParams) {
127140
conversationId: params.conversationId,
128141
});
129142
const requireMention = activation !== "always";
130-
const selfJid = params.msg.selfJid?.replace(/:\d+/, "");
131-
const selfLid = params.msg.selfLid?.replace(/:\d+/, "");
132-
const replySenderJid = params.msg.replyToSenderJid?.replace(/:\d+/, "");
133-
const selfE164 = params.msg.selfE164 ? normalizeE164(params.msg.selfE164) : null;
134-
const replySenderE164 = params.msg.replyToSenderE164
135-
? normalizeE164(params.msg.replyToSenderE164)
136-
: null;
143+
const replyContext = getReplyContext(params.msg, params.authDir);
137144
// Detect reply-to-bot: compare JIDs, LIDs, and E.164 numbers.
138145
// WhatsApp may report the quoted message sender as either a phone JID
139146
// (xxxxx@s.whatsapp.net) or a LID (xxxxx@lid), so we compare both.
140-
const implicitMention = Boolean(
141-
(selfJid && replySenderJid && selfJid === replySenderJid) ||
142-
(selfLid && replySenderJid && selfLid === replySenderJid) ||
143-
(selfE164 && replySenderE164 && selfE164 === replySenderE164),
144-
);
147+
const implicitMention = identitiesOverlap(self, replyContext?.sender);
145148
const mentionGate = resolveMentionGating({
146149
requireMention,
147150
canDetectMention: true,

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import {
44
type EnvelopeFormatOptions,
55
} from "openclaw/plugin-sdk/channel-inbound";
66
import type { loadConfig } from "openclaw/plugin-sdk/config-runtime";
7+
import { getPrimaryIdentityId, getReplyContext, getSenderIdentity } from "../../identity.js";
78
import type { WebInboundMsg } from "../types.js";
89

910
export function formatReplyContext(msg: WebInboundMsg) {
10-
if (!msg.replyToBody) {
11+
const replyTo = getReplyContext(msg);
12+
if (!replyTo?.body) {
1113
return null;
1214
}
13-
const sender = msg.replyToSender ?? "unknown sender";
14-
const idPart = msg.replyToId ? ` id:${msg.replyToId}` : "";
15-
return `[Replying to ${sender}${idPart}]\n${msg.replyToBody}\n[/Replying]`;
15+
const sender = replyTo.sender?.label ?? replyTo.sender?.e164 ?? "unknown sender";
16+
const idPart = replyTo.id ? ` id:${replyTo.id}` : "";
17+
return `[Replying to ${sender}${idPart}]\n${replyTo.body}\n[/Replying]`;
1618
}
1719

1820
export function buildInboundLine(params: {
@@ -31,6 +33,7 @@ export function buildInboundLine(params: {
3133
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
3234
const replyContext = formatReplyContext(msg);
3335
const baseLine = `${prefixStr}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
36+
const sender = getSenderIdentity(msg);
3437

3538
// Wrap with standardized envelope for the agent.
3639
return formatInboundEnvelope({
@@ -40,9 +43,9 @@ export function buildInboundLine(params: {
4043
body: baseLine,
4144
chatType: msg.chatType,
4245
sender: {
43-
name: msg.senderName,
44-
e164: msg.senderE164,
45-
id: msg.senderJid,
46+
name: sender.name ?? undefined,
47+
e164: sender.e164 ?? undefined,
48+
id: getPrimaryIdentityId(sender) ?? undefined,
4649
},
4750
previousTimestamp,
4851
envelope,

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
55
import { buildGroupHistoryKey } from "openclaw/plugin-sdk/routing";
66
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
77
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
8+
import { getPrimaryIdentityId, getSenderIdentity } from "../../identity.js";
89
import type { MentionConfig } from "../mentions.js";
910
import type { WebInboundMsg } from "../types.js";
1011
import { maybeBroadcastMessage } from "./broadcast.js";
@@ -96,6 +97,7 @@ export function createWebOnMessageHandler(params: {
9697
}
9798

9899
if (msg.chatType === "group") {
100+
const sender = getSenderIdentity(msg);
99101
const metaCtx = {
100102
From: msg.from,
101103
To: msg.to,
@@ -104,9 +106,9 @@ export function createWebOnMessageHandler(params: {
104106
ChatType: msg.chatType,
105107
ConversationLabel: conversationId,
106108
GroupSubject: msg.groupSubject,
107-
SenderName: msg.senderName,
108-
SenderId: msg.senderJid?.trim() || msg.senderE164,
109-
SenderE164: msg.senderE164,
109+
SenderName: sender.name ?? undefined,
110+
SenderId: getPrimaryIdentityId(sender) ?? undefined,
111+
SenderE164: sender.e164 ?? undefined,
110112
Provider: "whatsapp",
111113
Surface: "whatsapp",
112114
OriginatingChannel: "whatsapp",
@@ -144,8 +146,12 @@ export function createWebOnMessageHandler(params: {
144146
}
145147
} else {
146148
// Ensure `peerId` for DMs is stable and stored as E.164 when possible.
147-
if (!msg.senderE164 && peerId && peerId.startsWith("+")) {
148-
msg.senderE164 = normalizeE164(peerId) ?? msg.senderE164;
149+
if (!msg.sender?.e164 && !msg.senderE164 && peerId && peerId.startsWith("+")) {
150+
const normalized = normalizeE164(peerId);
151+
if (normalized) {
152+
msg.sender = { ...(msg.sender ?? {}), e164: normalized };
153+
msg.senderE164 = normalized;
154+
}
149155
}
150156
}
151157

0 commit comments

Comments
 (0)