From a9ba11976ee050c14deede75ff798c46087ea6aa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:46:10 +0200 Subject: [PATCH 1/3] chore: update WhatsApp Web version (#2330) Co-authored-by: github-actions[bot] --- src/Defaults/baileys-version.json | 2 +- src/Defaults/index.ts | 2 +- src/Utils/generics.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Defaults/baileys-version.json b/src/Defaults/baileys-version.json index edf42218..978243b1 100644 --- a/src/Defaults/baileys-version.json +++ b/src/Defaults/baileys-version.json @@ -1 +1 @@ -{"version":[2,3000,1032141294]} +{"version":[2,3000,1033105955]} diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index be3750d9..fd6f6cb3 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -4,7 +4,7 @@ import type { AuthenticationState, SocketConfig, WAVersion } from '../Types' import { Browsers } from '../Utils/browser-utils' import logger from '../Utils/logger' -const version = [2, 3000, 1032141294] +const version = [2, 3000, 1033105955] export const UNAUTHORIZED_CODES = [401, 403, 419] diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 803517c2..9085224c 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -1,7 +1,7 @@ import { Boom } from '@hapi/boom' import { createHash, randomBytes } from 'crypto' import { proto } from '../../WAProto/index.js' -const baileysVersion = [2, 3000, 1032141294] +const baileysVersion = [2, 3000, 1033105955] import type { BaileysEventEmitter, BaileysEventMap, From 23156c833ebbe5d328e66d884d7d44bb40b81821 Mon Sep 17 00:00:00 2001 From: vini Date: Tue, 10 Feb 2026 21:11:16 -0300 Subject: [PATCH 2/3] feat(call): add caller phone number to offer call event (#2190) --- src/Socket/messages-recv.ts | 2 ++ src/Types/Call.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 9bedbd26..eeb335e4 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -1349,6 +1349,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const call: WACallEvent = { chatId: attrs.from!, from, + callerPn: infoChild.attrs['caller_pn'], id: callId, date: new Date(+attrs.t! * 1000), offline: !!attrs.offline, @@ -1368,6 +1369,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { if (existingCall) { call.isVideo = existingCall.isVideo call.isGroup = existingCall.isGroup + call.callerPn = call.callerPn || existingCall.callerPn } // delete data once call has ended diff --git a/src/Types/Call.ts b/src/Types/Call.ts index 80972f62..52558972 100644 --- a/src/Types/Call.ts +++ b/src/Types/Call.ts @@ -3,6 +3,7 @@ export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'acc export type WACallEvent = { chatId: string from: string + callerPn?: string isGroup?: boolean groupJid?: string id: string From 7a5b09061664f5c37d60faebc7771042b26fb059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:56:23 -0300 Subject: [PATCH 3/3] fix: request placeholder resend for messages without encryption (CTWAads) (#2334) * fix: request placeholder resend for messages without encryption (CTWA ads) * fix: implement placeholder resend cache management and metadata preservation --- src/Defaults/index.ts | 3 + src/Socket/messages-recv.ts | 174 ++++++++++++++++++++++++----------- src/Utils/process-message.ts | 48 ++++++++-- 3 files changed, 163 insertions(+), 62 deletions(-) diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index fd6f6cb3..0969ee8d 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -25,6 +25,9 @@ export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60 /** Status messages older than 24 hours are considered expired */ export const STATUS_EXPIRY_SECONDS = 24 * 60 * 60 +/** WA Web enforces a 14-day maximum age for placeholder resend requests */ +export const PLACEHOLDER_MAX_AGE_SECONDS = 14 * 24 * 60 * 60 + export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0' export const DICT_VERSION = 3 export const KEY_BUNDLE_TYPE = Buffer.from([5]) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index eeb335e4..2c1a5776 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -3,7 +3,13 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import Long from 'long' import { proto } from '../../WAProto/index.js' -import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, STATUS_EXPIRY_SECONDS } from '../Defaults' +import { + DEFAULT_CACHE_TTLS, + KEY_BUNDLE_TYPE, + MIN_PREKEY_COUNT, + PLACEHOLDER_MAX_AGE_SECONDS, + STATUS_EXPIRY_SECONDS +} from '../Defaults' import type { GroupParticipant, MessageReceiptType, @@ -141,7 +147,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { return sendPeerDataOperationMessage(pdoMessage) } - const requestPlaceholderResend = async (messageKey: WAMessageKey): Promise => { + const requestPlaceholderResend = async ( + messageKey: WAMessageKey, + msgData?: Partial + ): Promise => { if (!authState.creds.me?.id) { throw new Boom('Not authenticated') } @@ -150,7 +159,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { logger.debug({ messageKey }, 'already requested resend') return } else { - await placeholderResendCache.set(messageKey?.id!, true) + // Store original message data so PDO response handler can preserve + // metadata (LID details, timestamps, etc.) that the phone may omit + await placeholderResendCache.set(messageKey?.id!, msgData || true) } await delay(2000) @@ -1227,66 +1238,123 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) { - return sendMessageAck(node) - } - - // Skip retry for expired status messages (>24h old) - if (isJidStatusBroadcast(msg.key.remoteJid!)) { - const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp) - if (messageAge > STATUS_EXPIRY_SECONDS) { + // Message arrived without encryption (e.g. CTWA ads messages). + // Check if this is eligible for placeholder resend (matching WA Web filters). + const unavailableNode = getBinaryNodeChild(node, 'unavailable') + const unavailableType = unavailableNode?.attrs?.type + if ( + unavailableType === 'bot_unavailable_fanout' || + unavailableType === 'hosted_unavailable_fanout' || + unavailableType === 'view_once_unavailable_fanout' + ) { logger.debug( - { msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, - 'skipping retry for expired status message' + { msgId: msg.key.id, unavailableType }, + 'skipping placeholder resend for excluded unavailable type' ) return sendMessageAck(node) } - } - const errorMessage = msg?.messageStubParameters?.[0] || '' - const isPreKeyError = errorMessage.includes('PreKey') + const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp) + if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { + logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message') + return sendMessageAck(node) + } - logger.debug(`[handleMessage] Attempting retry request for failed decryption`) - - // Handle both pre-key and normal retries in single mutex - await retryMutex.mutex(async () => { - try { - if (!ws.isOpen) { - logger.debug({ node }, 'Connection closed, skipping retry') - return - } - - // Handle pre-key errors with upload and delay - if (isPreKeyError) { - logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying') - - try { - logger.debug('Uploading pre-keys for error recovery') - await uploadPreKeys(5) - logger.debug('Waiting for server to process new pre-keys') - await delay(1000) - } catch (uploadErr) { - logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway') + // Request the real content from the phone via placeholder resend PDO. + // Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg), + // and store the requestId in stubParameters[1] so users can correlate + // with the incoming PDO response event. + const cleanKey: proto.IMessageKey = { + remoteJid: msg.key.remoteJid, + fromMe: msg.key.fromMe, + id: msg.key.id, + participant: msg.key.participant + } + // Cache the original message metadata so the PDO response handler + // can preserve key fields (LID details etc.) that the phone may omit + const msgData: Partial = { + key: msg.key, + messageTimestamp: msg.messageTimestamp, + pushName: msg.pushName, + participant: msg.participant, + verifiedBizName: msg.verifiedBizName + } + requestPlaceholderResend(cleanKey, msgData) + .then(requestId => { + if (requestId && requestId !== 'RESOLVED') { + logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message') + ev.emit('messages.update', [ + { + key: msg.key, + update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] } + } + ]) } - } - - const encNode = getBinaryNodeChild(node, 'enc') - await sendRetryRequest(node, !encNode) - if (retryRequestDelayMs) { - await delay(retryRequestDelayMs) - } - } catch (err) { - logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry') - // Still attempt retry even if pre-key upload failed - try { - const encNode = getBinaryNodeChild(node, 'enc') - await sendRetryRequest(node, !encNode) - } catch (retryErr) { - logger.error({ retryErr }, 'Failed to send retry after error handling') + }) + .catch(err => { + logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message') + }) + await sendMessageAck(node) + // Don't return — fall through to upsertMessage so the stub is emitted + } else { + // Skip retry for expired status messages (>24h old) + if (isJidStatusBroadcast(msg.key.remoteJid!)) { + const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp) + if (messageAge > STATUS_EXPIRY_SECONDS) { + logger.debug( + { msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, + 'skipping retry for expired status message' + ) + return sendMessageAck(node) } } - await sendMessageAck(node, NACK_REASONS.UnhandledError) - }) + const errorMessage = msg?.messageStubParameters?.[0] || '' + const isPreKeyError = errorMessage.includes('PreKey') + + logger.debug(`[handleMessage] Attempting retry request for failed decryption`) + + // Handle both pre-key and normal retries in single mutex + await retryMutex.mutex(async () => { + try { + if (!ws.isOpen) { + logger.debug({ node }, 'Connection closed, skipping retry') + return + } + + // Handle pre-key errors with upload and delay + if (isPreKeyError) { + logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying') + + try { + logger.debug('Uploading pre-keys for error recovery') + await uploadPreKeys(5) + logger.debug('Waiting for server to process new pre-keys') + await delay(1000) + } catch (uploadErr) { + logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway') + } + } + + const encNode = getBinaryNodeChild(node, 'enc') + await sendRetryRequest(node, !encNode) + if (retryRequestDelayMs) { + await delay(retryRequestDelayMs) + } + } catch (err) { + logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry') + // Still attempt retry even if pre-key upload failed + try { + const encNode = getBinaryNodeChild(node, 'enc') + await sendRetryRequest(node, !encNode) + } catch (retryErr) { + logger.error({ retryErr }, 'Failed to send retry after error handling') + } + } + + await sendMessageAck(node, NACK_REASONS.UnhandledError) + }) + } } else { if (messageRetryManager && msg.key.id) { messageRetryManager.cancelPendingPhoneRequest(msg.key.id) diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index f57e9127..84399445 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -346,21 +346,51 @@ const processMessage = async ( case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE: const response = protocolMsg.peerDataOperationRequestResponseMessage! if (response) { - await placeholderResendCache?.del(response.stanzaId!) // TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.). - const { peerDataOperationResult } = response - for (const result of peerDataOperationResult!) { - const { placeholderMessageResendResponse: retryResponse } = result + const peerDataOperationResult = response.peerDataOperationResult || [] + for (const result of peerDataOperationResult) { + const retryResponse = result?.placeholderMessageResendResponse //eslint-disable-next-line max-depth - if (retryResponse) { - const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes!) - // wait till another upsert event is available, don't want it to be part of the PDO response message - // TODO: parse through proper message handling utilities (to add relevant key fields) + if (!retryResponse?.webMessageInfoBytes) { + continue + } + + //eslint-disable-next-line max-depth + try { + const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes) + const msgId = webMessageInfo.key?.id + // Retrieve cached original message data (preserves LID details, + // timestamps, etc. that the phone may omit in its PDO response) + const cachedData = msgId ? await placeholderResendCache?.get | true>(msgId) : undefined + //eslint-disable-next-line max-depth + if (msgId) { + await placeholderResendCache?.del(msgId) + } + + let finalMsg: WAMessage + //eslint-disable-next-line max-depth + if (cachedData && typeof cachedData === 'object') { + // Apply decoded message content onto cached metadata (preserves LID etc.) + cachedData.message = webMessageInfo.message + //eslint-disable-next-line max-depth + if (webMessageInfo.messageTimestamp) { + cachedData.messageTimestamp = webMessageInfo.messageTimestamp + } + + finalMsg = cachedData as WAMessage + } else { + finalMsg = webMessageInfo as WAMessage + } + + logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend') + ev.emit('messages.upsert', { - messages: [webMessageInfo as WAMessage], + messages: [finalMsg], type: 'notify', requestId: response.stanzaId! }) + } catch (err) { + logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response') } } }