fix: resolve WhatsApp LID to phone number for inbound DMs (#510)

This commit is contained in:
Cameron
2026-03-06 11:07:32 -08:00
committed by GitHub
parent 9da79aef09
commit 53c24bcdc7
2 changed files with 207 additions and 6 deletions

View File

@@ -0,0 +1,118 @@
import { describe, expect, it, vi } from 'vitest';
import type { GroupMetaCache } from '../utils.js';
import { extractInboundMessage } from './extract.js';
const REMOTE_LID = '210501234567890@lid';
function createMessage(
overrides: {
key?: Record<string, unknown>;
message?: Record<string, unknown>;
messageTimestamp?: number;
pushName?: string;
} = {}
): Record<string, unknown> {
const { key: keyOverrides, message: messageOverrides, ...rest } = overrides;
return {
key: {
remoteJid: REMOTE_LID,
id: 'msg-1',
...keyOverrides,
},
message: {
conversation: 'hello from web',
...messageOverrides,
},
messageTimestamp: 1700000000,
pushName: 'Alice',
...rest,
};
}
function createSocket(options: { lidMapping?: Map<string, string> } = {}): Record<string, unknown> {
return {
user: { id: '19998887777@s.whatsapp.net' },
signalRepository: options.lidMapping ? { lidMapping: options.lidMapping } : {},
groupMetadata: vi.fn(),
};
}
function createGroupMetaCache(): GroupMetaCache {
return {
get: vi.fn(async () => ({ expires: Date.now() + 60_000 })),
clear: vi.fn(),
};
}
describe('extractInboundMessage (LID DM resolution)', () => {
it('resolves LID DMs via senderPn and normalizes chatId to a PN JID', async () => {
const msg = createMessage({
key: {
senderPn: '15551234567@s.whatsapp.net',
},
});
const sock = createSocket();
const extracted = await extractInboundMessage(
msg as any,
sock as any,
createGroupMetaCache()
);
expect(extracted?.chatId).toBe('15551234567@s.whatsapp.net');
expect(extracted?.from).toBe('15551234567');
expect(extracted?.senderE164).toBe('15551234567');
});
it('falls back to signalRepository.lidMapping when senderPn is missing', async () => {
const msg = createMessage();
const sock = createSocket({
lidMapping: new Map([[REMOTE_LID, '16667778888@s.whatsapp.net']]),
});
const extracted = await extractInboundMessage(
msg as any,
sock as any,
createGroupMetaCache()
);
expect(extracted?.chatId).toBe('16667778888@s.whatsapp.net');
expect(extracted?.from).toBe('16667778888');
expect(extracted?.senderE164).toBe('16667778888');
});
it('falls back to the LID-derived number when no mapping is available', async () => {
const msg = createMessage();
const sock = createSocket();
const extracted = await extractInboundMessage(
msg as any,
sock as any,
createGroupMetaCache()
);
expect(extracted?.chatId).toBe(REMOTE_LID);
expect(extracted?.from).toBe('210501234567890');
expect(extracted?.senderE164).toBe('210501234567890');
});
it('accepts plain phone-number senderPn values by converting them to PN JIDs', async () => {
const msg = createMessage({
key: {
senderPn: '+1 (555) 222-3333',
},
});
const sock = createSocket();
const extracted = await extractInboundMessage(
msg as any,
sock as any,
createGroupMetaCache()
);
expect(extracted?.chatId).toBe('15552223333@s.whatsapp.net');
expect(extracted?.from).toBe('15552223333');
expect(extracted?.senderE164).toBe('15552223333');
});
});

View File

@@ -5,7 +5,7 @@
* Based on OpenClaw's extract.ts pattern.
*/
import { jidToE164, isGroupJid } from "../utils.js";
import { jidToE164, isGroupJid, isLid } from "../utils.js";
import type { WebInboundMessage, AttachmentExtractionConfig } from "./types.js";
import type { GroupMetaCache } from "../utils.js";
import { unwrapMessageContent, extractMediaPreview, collectAttachments } from "./media.js";
@@ -115,6 +115,69 @@ export function extractMentionedJids(message: import("@whiskeysockets/baileys").
* @param groupMetaCache - Group metadata cache
* @returns Normalized message or null if invalid
*/
/**
* Resolve an LID (Linked ID) to a real phone number JID for inbound messages.
*
* LIDs are privacy-focused identifiers used by WhatsApp Web. The same user
* has different JIDs depending on their device:
* - Phone app: 34600...@s.whatsapp.net (real phone number)
* - WhatsApp Web: xxxxx@lid (opaque linked ID)
*
* Without resolution, the same user gets different userIds, breaking
* debouncing, daily limits, and conversation routing.
*
* Resolution order:
* 1. msg.key.senderPn - provided by Baileys when available
* 2. sock.signalRepository.lidMapping - Baileys built-in mapping
* 3. Fall back to LID-stripped number (current behavior)
*
* @param lidJid - The LID JID to resolve (e.g., "12345@lid")
* @param msg - Baileys message (may contain senderPn)
* @param sock - Baileys socket (has signalRepository)
* @returns Resolved phone number JID or null if not resolvable
*/
function resolveLidToPhoneJid(
lidJid: string,
msg: import("@whiskeysockets/baileys").WAMessage,
sock: import("@whiskeysockets/baileys").WASocket
): string | null {
const normalizePhoneJid = (value: string | undefined): string | null => {
if (!value) return null;
const trimmed = value.trim();
if (!trimmed || isLid(trimmed)) {
return null;
}
if (trimmed.includes('@')) {
return trimmed;
}
// Defensive fallback: handle plain phone numbers by converting to a PN JID.
const digits = trimmed.replace(/[^\d]/g, '');
if (!digits) {
return null;
}
return `${digits}@s.whatsapp.net`;
};
// Try senderPn from message key (most reliable)
const senderPn = normalizePhoneJid(msg.key?.senderPn);
if (senderPn) {
return senderPn;
}
// Try signalRepository.lidMapping (Baileys built-in)
const signalRepo = sock.signalRepository as unknown as { lidMapping?: Map<string, string> } | undefined;
const signalMapping = normalizePhoneJid(signalRepo?.lidMapping?.get(lidJid));
if (signalMapping) {
return signalMapping;
}
// Could not resolve
return null;
}
export async function extractInboundMessage(
msg: import("@whiskeysockets/baileys").WAMessage,
sock: import("@whiskeysockets/baileys").WASocket,
@@ -163,10 +226,13 @@ export async function extractInboundMessage(
return null; // Skip messages with no text and no media
}
// Determine sender
// Determine sender and chatId
// For LID-based DMs, we need to resolve to the real phone number
// so the same user gets consistent userIds regardless of device
let from: string;
let senderE164: string | undefined;
let senderJid: string | undefined;
let resolvedChatId = remoteJid; // Normalized chat ID (phone JID for LID DMs)
if (isGroup) {
// Group message - sender is the participant
@@ -174,9 +240,26 @@ export async function extractInboundMessage(
senderJid = participantJid ? participantJid : undefined;
senderE164 = participantJid ? jidToE164(participantJid) : undefined;
} else {
// DM - sender is the remote JID
from = jidToE164(remoteJid);
senderE164 = from;
// DM - check if this is an LID that needs resolution
if (isLid(remoteJid)) {
// Try to resolve LID to real phone number JID
const resolvedJid = resolveLidToPhoneJid(remoteJid, msg, sock);
if (resolvedJid) {
// Successfully resolved - use real phone number
from = jidToE164(resolvedJid);
senderE164 = from;
resolvedChatId = resolvedJid; // Normalize chatId to phone JID
} else {
// Could not resolve - fall back to LID-stripped number
// This maintains backward compatibility but may cause user ID fragmentation
from = jidToE164(remoteJid);
senderE164 = from;
}
} else {
// Regular phone JID - use as-is
from = jidToE164(remoteJid);
senderE164 = from;
}
}
// Fetch group metadata if needed
@@ -209,7 +292,7 @@ export async function extractInboundMessage(
id: messageId ?? undefined,
from,
to: selfE164 ?? "me",
chatId: remoteJid,
chatId: resolvedChatId, // Use resolved chatId (phone JID for LID DMs)
body: finalBody,
pushName: msg.pushName ?? undefined,
timestamp: new Date(Number(msg.messageTimestamp) * 1000),