From d870ffb2a301cbd792732246fdf16d4dc09a2739 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Mon, 23 Mar 2026 00:02:54 -0300 Subject: [PATCH] fix: handle view-once send/recv on linked devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - messages-send.ts: for view-once messages, send DSM only to device=0 (primary phone). Companion devices (device>0) are omitted from participants — the WA server generates for them automatically. Sending explicit from a companion is rejected by the server and breaks delivery. phash is recalculated to match the actual participants sent. - messages-recv.ts: add early return for stanzas received by the companion (API). When the primary phone sends a view-once, linked companions receive this node with no enc content. Previously the code tried to decrypt it and crashed with a 500 error. Co-Authored-By: Renato Alcara --- src/Socket/messages-recv.ts | 11 +++++++++++ src/Socket/messages-send.ts | 22 +++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 9318a1b3..58848a05 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -2258,6 +2258,17 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { return } + // Handle view-once unavailable sync from primary device. + // When the primary device sends a view-once, linked companions receive + // — no enc content, just a sync signal. + // Acknowledge and skip decryption (nothing to decrypt). + const unavailableNode = getBinaryNodeChild(node, 'unavailable') + if (unavailableNode?.attrs?.type === 'view_once') { + logger.debug({ id: node.attrs.id, from: node.attrs.from }, 'received view_once unavailable sync from primary device') + await sendMessageAck(node) + return + } + const { fullMessage: msg, category, diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 211b6280..d1ec3132 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -1279,19 +1279,35 @@ export const makeMessagesSocket = (config: SocketConfig) => { await assertSessions(effectiveAllRecipients) + // For view-once: only send DSM to primary phone (device=0). + // Companion devices (device>0) are omitted — WA server generates + // for them automatically. + // Sending explicit from a companion is rejected by the server. + const isViewOnceMsg = !!( + message.viewOnceMessageV2 || + message.viewOnceMessage || + message.viewOnceMessageV2Extension + ) + const viewOnceMeRecipients = isViewOnceMsg + ? effectiveMeRecipients.filter(jid => !jidDecode(jid)?.device) + : effectiveMeRecipients + const [ { nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 } ] = await Promise.all([ // For own devices: use DSM (deviceSentMessage) wrapper - createParticipantNodes(effectiveMeRecipients, meMsg || message, extraAttrs), + createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs), createParticipantNodes(effectiveOtherRecipients, message, extraAttrs) ]) participants.push(...meNodes) participants.push(...otherNodes) - if (effectiveMeRecipients.length > 0 || effectiveOtherRecipients.length > 0) { - extraAttrs['phash'] = generateParticipantHashV2(effectiveAllRecipients) + const phashRecipients = isViewOnceMsg + ? [...viewOnceMeRecipients, ...effectiveOtherRecipients] + : effectiveAllRecipients + if (phashRecipients.length > 0) { + extraAttrs['phash'] = generateParticipantHashV2(phashRecipients) } shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2