From 88313ce1028d5c7a134f223899bf24148b640804 Mon Sep 17 00:00:00 2001 From: rmyndharis Date: Sat, 20 Jun 2026 08:39:07 +0700 Subject: [PATCH] fix(baileys): resolve saved/contact names for the Chats list (#369) When Baileys gave a chat no title, the Chats list fell back to the raw JID user-part (a bare number, or a privacy-id for @lid contacts). Resolve a best-known display name from the synced contacts (saved name -> verifiedName -> pushName), and for a @lid chat look up the contact behind the resolved phone, keeping the raw user-part only as a last resort. No API shape change; ChatSummary.name is simply better populated. --- CHANGELOG.md | 8 ++++ .../adapters/baileys-session-store.spec.ts | 32 ++++++++++++++++ src/engine/adapters/baileys-session-store.ts | 37 ++++++++++++++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f44ebac..23673344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Baileys engine: the Chats list now shows saved/contact names instead of a raw number or `@lid`.** When + Baileys supplied a chat without a title, the dashboard Chats list fell back to the raw JID user-part (a + bare number, or a privacy-id for `@lid` contacts). The session store now resolves a best-known display + name from the synced contacts — preferring the saved name, then the business `verifiedName`, then the + pushName (`notify`) — and for a `@lid` chat it also looks up the contact behind the resolved phone. The + 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: 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 a0a58c04..ec1b6598 100644 --- a/src/engine/adapters/baileys-session-store.spec.ts +++ b/src/engine/adapters/baileys-session-store.spec.ts @@ -96,6 +96,38 @@ describe('BaileysSessionStore', () => { expect(store.resolvePhone('111:7@lid')).toBe('628999'); }); + describe('toNeutralChat contact-name resolution (#369)', () => { + it('keeps the chat title when Baileys supplies one (it wins over the contact)', () => { + store.upsertChats([{ id: '628111@s.whatsapp.net', name: 'Chat Title' }]); + store.upsertContacts([{ id: '628111@s.whatsapp.net', name: 'Saved Name' }]); + expect(store.listChats()[0].name).toBe('Chat Title'); + }); + + it('falls back to the saved contact name for a titleless bare-number chat', () => { + store.upsertChats([{ id: '628111@s.whatsapp.net' }]); // no chat title + store.upsertContacts([{ id: '628111@s.whatsapp.net', name: 'Alice' }]); + expect(store.listChats()[0].name).toBe('Alice'); + }); + + it('resolves a saved name for a @lid chat via the lid->pn mapping', () => { + store.upsertChats([{ id: '111@lid' }]); // titleless, lid-keyed + store.addLidMappings([{ lid: '111@lid', pn: '628999@s.whatsapp.net' }]); + store.upsertContacts([{ id: '628999@s.whatsapp.net', name: 'Carol' }]); + expect(store.listChats()[0].name).toBe('Carol'); + }); + + it('uses pushName (notify) when no saved/verified name exists', () => { + store.upsertChats([{ id: '628222@s.whatsapp.net' }]); + store.upsertContacts([{ id: '628222@s.whatsapp.net', notify: 'Dave' }]); + expect(store.listChats()[0].name).toBe('Dave'); + }); + + it('falls back to the raw user-part when nothing is known (last resort)', () => { + store.upsertChats([{ id: '628333@s.whatsapp.net' }]); + expect(store.listChats()[0].name).toBe('628333'); + }); + }); + 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 9f80e621..397743c1 100644 --- a/src/engine/adapters/baileys-session-store.ts +++ b/src/engine/adapters/baileys-session-store.ts @@ -135,7 +135,7 @@ export class BaileysSessionStore { const last = this.lastMessages.get(c.id); return { id: c.id, - name: c.name ?? userPart(c.id), + name: c.name ?? this.resolveContactName(c.id), isGroup: c.id.endsWith('@g.us'), unreadCount: c.unreadCount ?? 0, timestamp: last?.timestamp ?? this.toUnixSeconds(c.conversationTimestamp), @@ -143,6 +143,41 @@ export class BaileysSessionStore { }; } + /** + * Best-known display name for a chat id when Baileys gave the chat no title (#369). Prefers the saved + * contact name, then verifiedName, then pushName (`notify`); for a @lid chat it also tries the contact + * behind the resolved phone. Falls back to the raw user-part so a number/lid is never shown as a JID. + */ + private resolveContactName(id: string): string { + const direct = this.contactDisplayName(id); + if (direct) { + return direct; + } + const parsed = parseWaId(id); + if (parsed.kind === 'lid') { + const lidJid = `${parsed.userPart}@lid`; + const pn = + this.lidToPn.get(lidJid) ?? + this.lidToPn.get(id) ?? + (this.contacts.get(lidJid) ?? this.contacts.get(id))?.phoneNumber; + if (pn) { + const viaPhone = + this.contactDisplayName(pn) ?? + this.contactDisplayName(`${userPart(pn)}@s.whatsapp.net`) ?? + this.contactDisplayName(`${userPart(pn)}@c.us`); + if (viaPhone) { + return viaPhone; + } + } + } + return userPart(id); + } + + private contactDisplayName(id: string): string | undefined { + const c = this.contacts.get(id); + return c ? (c.name ?? c.verifiedName ?? c.notify ?? undefined) : undefined; + } + private toUnixSeconds(ts: number | { toNumber(): number } | null | undefined): number { if (ts == null) { return 0;