diff --git a/CHANGELOG.md b/CHANGELOG.md index 952faf0b..04adc8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 raw user-part remains the last resort, so a name is shown whenever WhatsApp has delivered one. No API shape change (`ChatSummary.name` is simply better populated). (#369) +- **Baileys engine: `@lid` senders now resolve to a phone number.** `senderPhone` and + `GET /sessions/:id/contacts/:id/phone` always returned `null` for privacy-id (`@lid`) contacts on + Baileys: the resolver only consulted mappings from `contacts.*` / `messaging-history.set`, which don't + fire for a fresh inbound `@lid` sender, and baileys@6.7.23 has no `getPNForLID` lookup. The adapter now + learns the `lid -> phone` pair that Baileys attaches to the inbound message key (`senderPn` / + `participantPn`), so the sender of an incoming message resolves to its number and later contact lookups + succeed. Still best-effort by design — a number is only revealed once WhatsApp delivers the mapping + (e.g. an inbound message from that contact). (#362) + - **Baileys engine: inbound message ids are now engine-neutral (`@c.us`).** The Baileys adapter emitted its native `@s.whatsapp.net` / `@lid` ids in message payloads (`from` / `to` / `chatId` / `author`, plus revoked and reaction events), while the whatsapp-web.js engine and the rest of the diff --git a/src/engine/adapters/baileys-session-store.spec.ts b/src/engine/adapters/baileys-session-store.spec.ts index ec1b6598..fb78208b 100644 --- a/src/engine/adapters/baileys-session-store.spec.ts +++ b/src/engine/adapters/baileys-session-store.spec.ts @@ -128,6 +128,30 @@ describe('BaileysSessionStore', () => { }); }); + describe('recordKeyLidMappings (#362)', () => { + it('learns a lid->pn mapping from an inbound message key (senderLid/senderPn)', () => { + store.recordKeyLidMappings({ senderLid: '111@lid', senderPn: '628999@s.whatsapp.net' }); + expect(store.resolvePhone('111@lid')).toBe('628999'); + }); + + it('learns a group participant lid->pn mapping (participantLid/participantPn)', () => { + store.recordKeyLidMappings({ participantLid: '222@lid', participantPn: '628222@s.whatsapp.net' }); + expect(store.resolvePhone('222@lid')).toBe('628222'); + }); + + it('canonicalizes a @lid to @c.us once the key mapping is learned', () => { + expect(store.toNeutralJid('111@lid')).toBe('111@lid'); // unknown yet + store.recordKeyLidMappings({ senderLid: '111@lid', senderPn: '628111@s.whatsapp.net' }); + expect(store.toNeutralJid('111@lid')).toBe('628111@c.us'); + }); + + it('ignores a key with no lid/pn pair', () => { + store.recordKeyLidMappings({}); + store.recordKeyLidMappings({ senderLid: '333@lid' }); // lid without pn + expect(store.resolvePhone('333@lid')).toBeNull(); + }); + }); + describe('toNeutralJid', () => { it('maps @s.whatsapp.net to @c.us and strips the device suffix', () => { expect(store.toNeutralJid('628111@s.whatsapp.net')).toBe('628111@c.us'); diff --git a/src/engine/adapters/baileys-session-store.ts b/src/engine/adapters/baileys-session-store.ts index 397743c1..8d4db9c4 100644 --- a/src/engine/adapters/baileys-session-store.ts +++ b/src/engine/adapters/baileys-session-store.ts @@ -58,6 +58,20 @@ export class BaileysSessionStore { } } + /** + * Learn lid->pn mappings from an inbound message key (#362). Baileys attaches the sender's phone JID + * (`senderPn` / `participantPn`) next to its privacy id (`senderLid` / `participantLid`) on the message + * key — the only place a fresh `@lid` sender's number is revealed in @whiskeysockets/baileys@6.7.23 + * (there is no `getPNForLID` lookup and `contacts.*` / `messaging-history.set` don't fire for it). This + * lets `resolvePhone` (senderPhone, `GET /contacts/:id/phone`) and lid canonicalization succeed. + */ + recordKeyLidMappings(key: Pick): void { + this.addLidMappings([ + { lid: key.senderLid ?? undefined, pn: key.senderPn ?? undefined }, + { lid: key.participantLid ?? undefined, pn: key.participantPn ?? undefined }, + ]); + } + recordMessage(msg: WAMessage): void { const chatId = msg.key?.remoteJid; if (!chatId || !msg.key) { diff --git a/src/engine/adapters/baileys.adapter.spec.ts b/src/engine/adapters/baileys.adapter.spec.ts index 4bc27969..0f0cb940 100644 --- a/src/engine/adapters/baileys.adapter.spec.ts +++ b/src/engine/adapters/baileys.adapter.spec.ts @@ -610,6 +610,35 @@ describe('BaileysAdapter inbound fan-out', () => { expect(msg.isLidSender).toBe(true); // still flagged: the raw sender was a lid }); + it('resolves an @lid sender via the lid/pn pair carried on the inbound message key (#362)', async () => { + const onMessage = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize({ onMessage }); + // No history-sync mapping this time; the inbound key itself carries senderLid + senderPn, + // which is the only place a fresh @lid sender's number is revealed in baileys@6.7.23. + fakeSock.fire('messages.upsert', { + type: 'notify', + messages: [ + { + key: { + remoteJid: '111@lid', + fromMe: false, + id: 'IN_LID_KEY', + senderLid: '111@lid', + senderPn: '628111@s.whatsapp.net', + }, + message: { conversation: 'hi from lid' }, + messageTimestamp: 1700000005, + }, + ], + }); + await new Promise(r => setImmediate(r)); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const msg = onMessage.mock.calls[0][0] as { from: string; isLidSender?: boolean }; + expect(msg.from).toBe('628111@c.us'); // resolved from the key's senderPn, neutral dialect + expect(msg.isLidSender).toBe(true); + }); + it('keeps an unresolved @lid sender as @lid end-to-end (no mapping known)', async () => { const onMessage = jest.fn(); const adapter = newAdapter(); diff --git a/src/engine/adapters/baileys.adapter.ts b/src/engine/adapters/baileys.adapter.ts index 83320b00..bce4afd3 100644 --- a/src/engine/adapters/baileys.adapter.ts +++ b/src/engine/adapters/baileys.adapter.ts @@ -706,6 +706,9 @@ export class BaileysAdapter implements IWhatsAppEngine { try { const b = await this.loadLib(); const remoteJid = msg.key.remoteJid!; + // Learn any lid->pn pair the key carries BEFORE canonicalizing ids below, so a fresh @lid + // sender resolves to its phone in this message and for later contact lookups (#362). + this.sessionStore.recordKeyLidMappings(msg.key); const contentType = b.getContentType(msg.message ?? undefined); // --- protocolMessage REVOKE: don't emit onMessage ---