diff --git a/src/engine/adapters/baileys.adapter.spec.ts b/src/engine/adapters/baileys.adapter.spec.ts index 7399295e0..657f4cfce 100644 --- a/src/engine/adapters/baileys.adapter.spec.ts +++ b/src/engine/adapters/baileys.adapter.spec.ts @@ -666,6 +666,64 @@ describe('BaileysAdapter inbound fan-out', () => { expect(msg).toMatchObject({ id: 'IN1', body: 'hi there', type: 'text', fromMe: false }); }); + it('extracts coordinates from an ephemeral (disappearing) location message', async () => { + const onMessage = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize({ onMessage }); + const inner = { + locationMessage: { degreesLatitude: 24.1, degreesLongitude: 55.2, name: 'Office', address: '1 Main St' }, + }; + baileys.getContentType.mockReturnValue('locationMessage'); + baileys.normalizeMessageContent.mockReturnValue(inner); // unwrap the ephemeral wrapper + fakeSock.fire('messages.upsert', { + type: 'notify', + messages: [ + { + key: { remoteJid: '628111@s.whatsapp.net', fromMe: false, id: 'LOC1' }, + message: { ephemeralMessage: { message: inner } }, // wrapped location + messageTimestamp: 1700000002, + }, + ], + }); + await new Promise(r => setImmediate(r)); + expect(onMessage).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const msg = onMessage.mock.calls[0][0] as { location?: Record }; + expect(msg.location).toMatchObject({ + latitude: 24.1, + longitude: 55.2, + description: 'Office', + address: '1 Main St', + }); + }); + + it('maps an ephemeral-wrapped history message to its real type and body (not unknown/empty)', async () => { + const onHistoryMessages = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize({ onHistoryMessages }); + const inner = { conversation: 'disappearing hello' }; + baileys.normalizeMessageContent.mockReturnValue(inner); // unwrap the ephemeral wrapper + baileys.getContentType.mockReturnValue('conversation'); + fakeSock.fire('messaging-history.set', { + contacts: [], + chats: [], + messages: [ + { + key: { remoteJid: '628111@s.whatsapp.net', fromMe: false, id: 'H1' }, + message: { ephemeralMessage: { message: inner } }, + messageTimestamp: 1700000000, + pushName: 'Alice', + }, + ], + }); + await new Promise(r => setImmediate(r)); + await new Promise(r => setImmediate(r)); + expect(onHistoryMessages).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const mapped = onHistoryMessages.mock.calls[0][0] as Array<{ id: string; type: string; body: string }>; + expect(mapped[0]).toMatchObject({ id: 'H1', type: 'text', body: 'disappearing hello' }); + }); + it('surfaces inbound @mentions as neutral mentionedIds (contextInfo.mentionedJid)', 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 e8d30fe57..38c0352ac 100644 --- a/src/engine/adapters/baileys.adapter.ts +++ b/src/engine/adapters/baileys.adapter.ts @@ -265,7 +265,7 @@ export class BaileysAdapter implements IWhatsAppEngine { const h = history as unknown as { lidPnMappings?: { lid: string; pn: string }[]; syncType?: unknown }; const lidPnMappings = h.lidPnMappings; this.sessionStore.addLidMappings(lidPnMappings ?? []); - this.captureHistoryMessages(history.messages ?? []); + void this.captureHistoryMessages(history.messages ?? []); this.logger.debug('History sync received', { action: 'baileys_history_set', sessionId: this.config.sessionId, @@ -1074,9 +1074,11 @@ export class BaileysAdapter implements IWhatsAppEngine { // ILocationMessage has name/address; ILiveLocationMessage does not — use the static variant only. let location: IncomingMessage['location']; if (contentType === 'locationMessage' || contentType === 'liveLocationMessage') { - const lm = content.locationMessage ?? content.liveLocationMessage; + // Read off the NORMALIZED content: an ephemeral/disappearing-chat location nests under the wrapper, + // so the raw `content.locationMessage` is undefined and the coordinates would be silently dropped. + const lm = normalized.locationMessage ?? normalized.liveLocationMessage; if (lm) { - const staticLm = content.locationMessage; // only ILocationMessage has name/address + const staticLm = normalized.locationMessage; // only ILocationMessage has name/address location = { latitude: lm.degreesLatitude ?? 0, longitude: lm.degreesLongitude ?? 0, @@ -1239,10 +1241,11 @@ export class BaileysAdapter implements IWhatsAppEngine { * `onHistoryMessages` callback, harvesting `pushName` into contacts on the way (history `contacts` * carry no names) and seeding each chat's last-message preview. */ - private captureHistoryMessages(messages: WAMessage[]): void { + private async captureHistoryMessages(messages: WAMessage[]): Promise { if (!messages.length) { return; } + const b = await this.loadLib(); const nameUpdates: { id: string; notify: string }[] = []; const mapped: IncomingMessage[] = []; for (const msg of messages) { @@ -1255,7 +1258,7 @@ export class BaileysAdapter implements IWhatsAppEngine { // Seed the chat's last-message preview + sort time (newest wins); else history-only chats // would read "No messages yet". this.sessionStore.recordMessage(msg); - const incoming = this.mapHistoryMessage(msg); + const incoming = this.mapHistoryMessage(b, msg); if (incoming) { mapped.push(incoming); } @@ -1303,26 +1306,26 @@ export class BaileysAdapter implements IWhatsAppEngine { * messages would be ruinous; the type is kept, the payload dropped). Returns null for protocol / * reaction / key / empty messages, which carry nothing for the chat view. */ - private mapHistoryMessage(msg: WAMessage): IncomingMessage | null { - const content = msg.message; - if (!content || !msg.key?.remoteJid || !msg.key.id) { + private mapHistoryMessage(b: typeof BaileysLib, msg: WAMessage): IncomingMessage | null { + const raw = msg.message; + if (!raw || !msg.key?.remoteJid || !msg.key.id) { return null; } - const contentType = Object.keys(content)[0]; + // Unwrap ephemeral/viewOnce/documentWithCaption/edited wrappers so the real type and body surface — + // else a disappearing-chat message maps to type 'unknown' with an empty body. Identity no-op when + // already unwrapped. Derive ONE contentType from the normalized content for both the skip-filter and + // the type mapping, and reuse extractBaileysBody (the same body extraction the live path uses). + const content = b.normalizeMessageContent(raw) ?? raw; + const contentType = b.getContentType(content); if ( + !contentType || contentType === 'protocolMessage' || contentType === 'reactionMessage' || contentType === 'senderKeyDistributionMessage' ) { return null; } - const body = - content.conversation ?? - content.extendedTextMessage?.text ?? - content.imageMessage?.caption ?? - content.videoMessage?.caption ?? - content.documentMessage?.caption ?? - ''; + const body = extractBaileysBody(content); return buildIncomingMessageFromBaileys( { id: msg.key.id,