Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fac156474 | |||
| 77ffe59911 | |||
| 313f304ddf | |||
| a74fbb08a6 | |||
| 0e6bad72bb | |||
| 08c19e2951 | |||
| 90061dd6a1 | |||
| d014a2496e | |||
| 47b0940428 | |||
| 90937af374 |
@@ -1,7 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package proto;
|
package proto;
|
||||||
|
|
||||||
/// WhatsApp Version: 2.3000.1037082622
|
/// WhatsApp Version: 2.3000.1037162330
|
||||||
|
|
||||||
message ADVDeviceIdentity {
|
message ADVDeviceIdentity {
|
||||||
optional uint32 rawId = 1;
|
optional uint32 rawId = 1;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":[2,3000,1037137890]}
|
{"version":[2,3000,1037166441]}
|
||||||
|
|||||||
+19
-2
@@ -31,9 +31,26 @@ export const STATUS_EXPIRY_SECONDS = 24 * 60 * 60
|
|||||||
export const PLACEHOLDER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
|
export const PLACEHOLDER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
|
||||||
|
|
||||||
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
|
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
|
||||||
export const DICT_VERSION = 3
|
|
||||||
|
/**
|
||||||
|
* Protocol mode: 'web' (default) or 'native' (experimental)
|
||||||
|
* - web: WA\x06\x03 — standard WhatsApp Web protocol
|
||||||
|
* - native: WAM\x05 — native Android protocol (may enable view-once media on companions)
|
||||||
|
*
|
||||||
|
* Set via BAILEYS_PROTOCOL env var:
|
||||||
|
* BAILEYS_PROTOCOL=native
|
||||||
|
*/
|
||||||
|
const PROTOCOL_MODE = process.env.BAILEYS_PROTOCOL?.trim().toLowerCase() === 'native' ? 'native' : 'web'
|
||||||
|
|
||||||
|
export const DICT_VERSION = PROTOCOL_MODE === 'native' ? 5 : 3
|
||||||
export const KEY_BUNDLE_TYPE = Buffer.from([5])
|
export const KEY_BUNDLE_TYPE = Buffer.from([5])
|
||||||
export const NOISE_WA_HEADER = Buffer.from([87, 65, 6, DICT_VERSION]) // last is "DICT_VERSION"
|
|
||||||
|
// WA\x06\x03 = web protocol, WAM\x05 = native Android protocol
|
||||||
|
export const NOISE_WA_HEADER = PROTOCOL_MODE === 'native'
|
||||||
|
? Buffer.from([87, 65, 77, DICT_VERSION]) // WAM\x05
|
||||||
|
: Buffer.from([87, 65, 6, DICT_VERSION]) // WA\x06\x03
|
||||||
|
|
||||||
|
export { PROTOCOL_MODE }
|
||||||
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
|
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
|
||||||
export const URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g
|
export const URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g
|
||||||
|
|
||||||
|
|||||||
+75
-14
@@ -2241,25 +2241,78 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMessage = async (node: BinaryNode) => {
|
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) {
|
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')
|
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)
|
await sendMessageAck(node)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
// msmsg block removed — allow msmsg decryption (required for view-once on linked devices)
|
|
||||||
|
|
||||||
// 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')
|
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') {
|
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)
|
await sendMessageAck(node)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2377,12 +2430,20 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
{ msgId: msg.key?.id, messageType },
|
{ msgId: msg.key?.id, messageType },
|
||||||
'CTWA: Skipping placeholder resend for unavailable fanout type'
|
'CTWA: Skipping placeholder resend for unavailable fanout type'
|
||||||
)
|
)
|
||||||
// For view-once: emit the message as a stub so consumers know it arrived.
|
// For view-once: emit the message with a viewOnceMessage placeholder
|
||||||
// Use 'notify' for live messages so realtime consumers get the event.
|
// so consumers (zpro, etc.) can detect and display it.
|
||||||
|
// Include a message wrapper so isValidMsg accepts it.
|
||||||
if (messageType === 'view_once_unavailable_fanout') {
|
if (messageType === 'view_once_unavailable_fanout') {
|
||||||
msg.key.isViewOnce = true
|
msg.key.isViewOnce = true
|
||||||
msg.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
|
msg.message = {
|
||||||
msg.messageStubParameters = ['view_once_unavailable']
|
viewOnceMessage: {
|
||||||
|
message: {
|
||||||
|
imageMessage: {
|
||||||
|
viewOnce: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const upsertType = node.attrs.offline ? 'append' : 'notify'
|
const upsertType = node.attrs.offline ? 'append' : 'notify'
|
||||||
await upsertMessage(msg, upsertType)
|
await upsertMessage(msg, upsertType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createHash } from 'crypto'
|
|||||||
import { proto } from '../../WAProto/index.js'
|
import { proto } from '../../WAProto/index.js'
|
||||||
import {
|
import {
|
||||||
KEY_BUNDLE_TYPE,
|
KEY_BUNDLE_TYPE,
|
||||||
|
PROTOCOL_MODE,
|
||||||
WA_ADV_ACCOUNT_SIG_PREFIX,
|
WA_ADV_ACCOUNT_SIG_PREFIX,
|
||||||
WA_ADV_DEVICE_SIG_PREFIX,
|
WA_ADV_DEVICE_SIG_PREFIX,
|
||||||
WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX
|
WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX
|
||||||
@@ -58,7 +59,10 @@ const getClientPayload = (config: SocketConfig) => {
|
|||||||
userAgent: getUserAgent(config)
|
userAgent: getUserAgent(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.webInfo = getWebInfo(config)
|
// Native protocol (WAM\x05) does not use WebInfo — only web protocol does
|
||||||
|
if (PROTOCOL_MODE !== 'native') {
|
||||||
|
payload.webInfo = getWebInfo(config)
|
||||||
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user