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
58 changes: 58 additions & 0 deletions src/engine/adapters/baileys.adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> };
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();
Expand Down
35 changes: 19 additions & 16 deletions src/engine/adapters/baileys.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<void> {
if (!messages.length) {
return;
}
const b = await this.loadLib();
const nameUpdates: { id: string; notify: string }[] = [];
const mapped: IncomingMessage[] = [];
for (const msg of messages) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down
Loading