fix(whatsapp): peel nested envelopes and restore immediate payload return

A quoted message can carry its own envelope stack (ephemeral
wrapping viewOnce wrapping the payload), and a reply itself can be
enveloped: both layers now unwrap iteratively (bounded) before quote
extraction. Peeled top-level envelopes return their inner message
immediately, as before — the template/buttons/list branches only
apply to unenveloped messages.
This commit is contained in:
Konstantin Khlopkov
2026-09-09 11:34:18 +03:00
committed by Teknium
parent 5de769956f
commit 7ecd4d14a5
2 changed files with 55 additions and 5 deletions
@@ -468,4 +468,43 @@ import {
console.log(' ✓ quoted message text survives ephemeral/view-once/document envelopes');
}
{
// Nested envelopes (ephemeral wrapping viewOnce wrapping the payload) and
// the second envelope position: quoted text must resolve there as well.
const quotedMessage = {
ephemeralMessage: {
message: {
viewOnceMessageV2: {
message: { extendedTextMessage: { text: 'Example appointment at 11:40' } },
},
},
},
};
const event = await extractBridgeEvent({
msg: {
key: { id: 'reply-nested', remoteJid: '15551234567@s.whatsapp.net', fromMe: false },
messageTimestamp: 123,
message: {
ephemeralMessage: {
message: {
extendedTextMessage: {
text: 'confirmed',
contextInfo: { stanzaId: 'original-id', participant: '15559998888@s.whatsapp.net', quotedMessage },
},
},
},
},
},
chatId: '15551234567@s.whatsapp.net',
senderId: '15550001111@s.whatsapp.net',
senderNumber: '15550001111',
botIds: ['15559998888@s.whatsapp.net'],
downloadMedia: async () => Buffer.from(''),
});
assert.equal(event.quotedMessageId, 'original-id');
assert.equal(event.hasQuotedMessage, true);
assert.equal(event.quotedText, 'Example appointment at 11:40');
console.log(' ✓ nested envelopes: quote text resolves through both layers');
}
console.log('\n✅ All WhatsApp native bridge helper tests passed.');
+16 -5
View File
@@ -19,15 +19,26 @@ export function normalizeWhatsAppId(value) {
}
function unwrapMessageEnvelopes(content) {
if (content?.ephemeralMessage?.message) return content.ephemeralMessage.message;
if (content?.viewOnceMessage?.message) return content.viewOnceMessage.message;
if (content?.viewOnceMessageV2?.message) return content.viewOnceMessageV2.message;
if (content?.documentWithCaptionMessage?.message) return content.documentWithCaptionMessage.message;
return content;
let cur = content;
// Envelopes nest (ephemeral wrapping viewOnce wrapping the payload); peel
// until an inner message is reached so nested quotes resolve too.
for (let i = 0; i < 8 && cur; i++) {
const next =
cur.ephemeralMessage?.message ??
cur.viewOnceMessage?.message ??
cur.viewOnceMessageV2?.message ??
cur.documentWithCaptionMessage?.message;
if (next === undefined) break;
cur = next;
}
return cur;
}
export function getMessageContent(msg) {
const content = unwrapMessageEnvelopes(msg?.message || {});
// A peeled envelope returns its inner message immediately: the payload
// shape (template/buttons/list/etc.) applies to unenveloped messages.
if (content !== (msg?.message || {})) return content;
if (content.templateMessage?.hydratedTemplate) return content.templateMessage.hydratedTemplate;
if (content.buttonsMessage) return content.buttonsMessage;
if (content.listMessage) return content.listMessage;