Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<phone>@s.whatsapp.net` / `<lid>@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
Expand Down
24 changes: 24 additions & 0 deletions src/engine/adapters/baileys-session-store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <phone>@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');
Expand Down
14 changes: 14 additions & 0 deletions src/engine/adapters/baileys-session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WAMessageKey, 'senderLid' | 'senderPn' | 'participantLid' | 'participantPn'>): 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) {
Expand Down
29 changes: 29 additions & 0 deletions src/engine/adapters/baileys.adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions src/engine/adapters/baileys.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand Down
Loading