From 3ab578aadf6c2bc7f0d0ef58baa26aadc68173d0 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Fri, 20 Mar 2026 10:11:49 -0300 Subject: [PATCH] fix: detect view-once media (image/video/audio) (#309) * fix: detect view-once media (image/video/audio) on stanza 1 for linked devices When a view-once arrives at a linked device (InfiniteAPI), the server sends: - Stanza 1: enc payload with full media metadata (mediaKey, directPath) wrapped in viewOnceMessage > imageMessage/videoMessage/audioMessage with viewOnce: true - Stanza 2: unavailable view_once fanout placeholder (already handled) Previously only stanza 2 set key.isViewOnce = true. Stanza 1 was emitted as plain media with no view-once indicator, making it indistinguishable from regular media on the consumer side (messages.upsert). This fix inspects the decrypted proto for viewOnceMessage (v1/v2/v2Ext) wrappers containing a media message with viewOnce=true and propagates key.isViewOnce=true. The viewOnceMessage wrapper is also used for interactive messages (carousel, buttons, lists) but those carry interactiveMessage/listMessage inside -- never imageMessage.viewOnce / videoMessage.viewOnce / audioMessage.viewOnce. The distinction is unambiguous and does not affect interactive message handling. Verified via WA Desktop --- src/Utils/decode-wa-message.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index bd2ded9e..4f00b1b9 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -392,6 +392,23 @@ export const decryptMessageNode = ( } else { fullMessage.message = msg } + + // Detect view-once media on stanza 1 received by linked device. + // viewOnceMessage wrapper is also used for interactive messages + // (interactiveMessage, listMessage, nativeFlowMessage) -- those do NOT have + // imageMessage.viewOnce / videoMessage.viewOnce / audioMessage.viewOnce = true. + // Only real view-once media carries viewOnce: true on the inner media message. + const viewOnceInner = + msg.viewOnceMessage?.message || + msg.viewOnceMessageV2?.message || + msg.viewOnceMessageV2Extension?.message + if ( + viewOnceInner?.imageMessage?.viewOnce || + viewOnceInner?.videoMessage?.viewOnce || + viewOnceInner?.audioMessage?.viewOnce + ) { + fullMessage.key.isViewOnce = true + } } catch (err: any) { // Check if this is a final failure after all retries exhausted const isRetryExhausted = err instanceof RetryExhaustedError