fix: handle view-once send/recv on linked devices

- 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 <unavailable type="view_once"/>
  for them automatically. Sending explicit <unavailable> 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 <unavailable type="view_once"/>
  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
This commit is contained in:
Renato Alcara
2026-03-23 00:02:54 -03:00
parent f6c72530a2
commit d870ffb2a3
2 changed files with 30 additions and 3 deletions
+11
View File
@@ -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
// <unavailable type="view_once"/> — 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,
+19 -3
View File
@@ -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
// <unavailable type="view_once"/> for them automatically.
// Sending explicit <unavailable> 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