Compare commits

...

10 Commits

Author SHA1 Message Date
Renato Alcara 230e08037b chore: update WhatsApp Web version to v2.3000.1037186195 (#359)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-13 06:33:07 -03:00
Renato Alcara 77ffe59911 Revert "feat: add WebdPayload with E2E media support and features bitmask"
This reverts commit 313f304ddf.
2026-04-13 00:01:32 -03:00
Renato Alcara 313f304ddf feat: add WebdPayload with E2E media support and features bitmask
Add webdPayload to WebInfo during connection with:
- supportsE2EImage/Video/Audio/Document = true
- supportsMediaRetry = true
- features bitmask with view-once capability flag

This may enable the WA server to deliver view-once media content
to Baileys companions instead of just <unavailable> placeholders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:43:17 -03:00
Renato Alcara a74fbb08a6 fix: request view-once content via PDO from primary phone
When receiving <unavailable type='view_once'/>, attempt to request
the actual media content from the primary phone via PDO
(Peer Data Operation / PLACEHOLDER_MESSAGE_RESEND).

If the phone responds, the full media (url, mediaKey, directPath)
arrives via messages.upsert, allowing consumers to display the image.

Falls back to placeholder if PDO fails or phone doesn't respond.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:21:02 -03:00
Renato Alcara 0e6bad72bb fix: emit view-once placeholder from unavailable handler
The early unavailable handler at line ~2261 was intercepting
view_once stanzas and silently discarding them (sendMessageAck + return)
before the view_once_unavailable_fanout handler could emit them.

Now emits a viewOnceMessage placeholder with isViewOnce=true so
consumers receive the event in messages.upsert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:14:13 -03:00
Renato Alcara 08c19e2951 merge: resolve conflict in messages-recv.ts (keep viewOnceMessage placeholder) 2026-04-12 11:55:12 -03:00
Renato Alcara 90061dd6a1 fix: allow msmsg decryption and emit view-once unavailable fanout (#355)
Two issues found during view-once testing on linked devices:

1. msmsg block removal:
   The "temporary fix" that discarded all msmsg-type encrypted messages
   was also blocking legitimate view-once messages. On linked devices,
   view-once arrives as msmsg encryption type. The block was originally
   added to prevent crashes from missing messageSecret, but the existing
   try/catch in the decrypt path already handles those errors gracefully
   by marking the message as a CIPHERTEXT stub.

2. view_once_unavailable_fanout emission:
   When a linked device receives a view-once, the WA server sends only
   <unavailable type="view_once"/> (no <enc> with actual media content).
   Previously this was silently acknowledged without emitting any event,
   so consumers (zpro, astra-api, etc.) never knew a view-once arrived.
   Now it emits the message as a CIPHERTEXT stub with key.isViewOnce=true
   so consumers can display "view-once message received" in their UI.

Co-Authored-By: Renato Alcara
2026-04-12 11:46:42 -03:00
Renato Alcara d014a2496e fix: allow msmsg decryption and emit view-once unavailable fanout (#355)
Two issues found during view-once testing on linked devices:

1. msmsg block removal:
   The "temporary fix" that discarded all msmsg-type encrypted messages
   was also blocking legitimate view-once messages. On linked devices,
   view-once arrives as msmsg encryption type. The block was originally
   added to prevent crashes from missing messageSecret, but the existing
   try/catch in the decrypt path already handles those errors gracefully
   by marking the message as a CIPHERTEXT stub.

2. view_once_unavailable_fanout emission:
   When a linked device receives a view-once, the WA server sends only
   <unavailable type="view_once"/> (no <enc> with actual media content).
   Previously this was silently acknowledged without emitting any event,
   so consumers (zpro, astra-api, etc.) never knew a view-once arrived.
   Now it emits the message as a CIPHERTEXT stub with key.isViewOnce=true
   so consumers can display "view-once message received" in their UI.

Co-Authored-By: Renato Alcara
2026-04-12 10:02:30 -03:00
Renato Alcara 47b0940428 chore: update WhatsApp Web version to v2.3000.1037166441 (#357)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-12 06:15:16 -03:00
github-actions[bot] 90937af374 chore: update proto/version to v2.3000.1037162330 (#356)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-04-12 01:00:42 -03:00
3 changed files with 82 additions and 17 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;
/// WhatsApp Version: 2.3000.1037082622
/// WhatsApp Version: 2.3000.1037162330
message ADVDeviceIdentity {
optional uint32 rawId = 1;
+1 -1
View File
@@ -1 +1 @@
{"version":[2,3000,1037137890]}
{"version":[2,3000,1037186195]}
+80 -15
View File
@@ -2241,30 +2241,78 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
const handleMessage = async (node: BinaryNode) => {
const _encNode = getBinaryNodeChild(node, 'enc')
const _unavNode = getBinaryNodeChild(node, 'unavailable')
// VO_RAW: log BEFORE any filtering — captures every stanza that enters handleMessage
console.log(`[VO_RAW] id=${node.attrs.id} from=${node.attrs.from} type=${node.attrs.type} enc=${_encNode ? _encNode.attrs?.type : 'NO'} unavailable=${_unavNode ? _unavNode.attrs?.type : 'NO'} children=${Array.isArray(node.content) ? (node.content as BinaryNode[]).map(c => c.tag).join(',') : 'none'}`)
if (shouldIgnoreJid(node.attrs.from!) && node.attrs.from !== S_WHATSAPP_NET) {
console.log(`[VO_RAW] IGNORED by shouldIgnoreJid: ${node.attrs.from}`)
logger.trace({ from: node.attrs.from }, 'ignored message')
// Send a clean ACK (no error code) so the server considers the
// message delivered. Using error 500 (UnhandledError) previously
// caused the server to retry delivery, generating duplicate traffic.
await sendMessageAck(node)
return
}
const encNode = getBinaryNodeChild(node, 'enc')
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
if (encNode?.attrs.type === 'msmsg') {
logger.debug({ key: node.attrs.key }, 'ignored msmsg')
await sendMessageAck(node, NACK_REASONS.MissingMessageSecret)
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')
// DEBUG: log every incoming message stanza to trace view-once delivery
logger.info({
id: node.attrs.id,
from: node.attrs.from,
type: node.attrs.type,
hasEnc: !!encNode,
encType: encNode?.attrs?.type,
hasUnavailable: !!unavailableNode,
unavailableType: unavailableNode?.attrs?.type,
}, 'VO_TRACE: incoming message stanza')
// msmsg block removed — allow msmsg decryption (required for view-once on linked devices)
if (unavailableNode?.attrs?.type === 'view_once') {
logger.debug({ id: node.attrs.id, from: node.attrs.from }, 'received view_once unavailable sync from primary device')
const { fullMessage: voMsg } = decryptMessageNode(
node,
authState.creds.me!.id,
authState.creds.me!.lid || '',
signalRepository,
logger,
)
voMsg.key.isViewOnce = true
// Try to request the actual view-once content from the primary phone via PDO
// The phone may resend the full media (url, mediaKey, directPath, etc.)
console.log(`[VO_PDO] Requesting view-once content via PDO for ${voMsg.key.id} from ${voMsg.key.remoteJid}`)
try {
const requestId = await requestPlaceholderResend(voMsg.key, {
key: { ...voMsg.key },
pushName: voMsg.pushName,
messageTimestamp: voMsg.messageTimestamp,
participant: voMsg.key.participant,
participantAlt: voMsg.key.participantAlt
})
if (requestId && requestId !== 'RESOLVED') {
console.log(`[VO_PDO] PDO request sent! requestId=${requestId} — waiting for phone to resend`)
} else if (requestId === 'RESOLVED') {
console.log(`[VO_PDO] Message arrived during PDO delay!`)
} else {
console.log(`[VO_PDO] Already requested or no response`)
}
} catch (err: any) {
console.log(`[VO_PDO] PDO request failed: ${err.message} — falling back to placeholder`)
}
// Emit placeholder immediately so consumers see the notification
// If PDO succeeds, the real message will arrive via messages.upsert later
voMsg.message = {
viewOnceMessage: {
message: {
imageMessage: {
viewOnce: true,
}
}
}
}
const upsertType = node.attrs.offline ? 'append' : 'notify'
await upsertMessage(voMsg, upsertType)
await sendMessageAck(node)
return
}
@@ -2382,6 +2430,23 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
{ msgId: msg.key?.id, messageType },
'CTWA: Skipping placeholder resend for unavailable fanout type'
)
// For view-once: emit the message with a viewOnceMessage placeholder
// so consumers (zpro, etc.) can detect and display it.
// Include a message wrapper so isValidMsg accepts it.
if (messageType === 'view_once_unavailable_fanout') {
msg.key.isViewOnce = true
msg.message = {
viewOnceMessage: {
message: {
imageMessage: {
viewOnce: true,
}
}
}
}
const upsertType = node.attrs.offline ? 'append' : 'notify'
await upsertMessage(msg, upsertType)
}
metrics.ctwaRecoveryFailures.inc({ reason: 'unavailable_fanout' })
return sendMessageAck(node)
}