diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 0fa30452..d5772020 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -10,6 +10,7 @@ import type { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, + PlaceholderMessageData, SocketConfig, WACallEvent, WAMessage, @@ -155,15 +156,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { return sendPeerDataOperationMessage(pdoMessage) } - /** Metadata cached for placeholder resend to preserve original message details */ - type PlaceholderMessageData = { - key: WAMessageKey - pushName?: string | null - messageTimestamp?: number | Long | null - participant?: string | null - participantAlt?: string | null - } - const requestPlaceholderResend = async ( messageKey: WAMessageKey, msgData?: PlaceholderMessageData @@ -172,18 +164,19 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { throw new Boom('Not authenticated') } - const cachedData = await placeholderResendCache.get(messageKey?.id!) - if (cachedData) { + // Check if already requested using message ID to prevent duplicate PDO requests + const alreadyRequested = await placeholderResendCache.get(messageKey?.id!) + if (alreadyRequested) { logger.debug({ messageKey }, 'already requested resend') return - } else { - // Store full message data if provided, otherwise store true for backward compatibility - await placeholderResendCache.set(messageKey?.id!, msgData || true) } + // Temporarily mark as requested using message ID to prevent race conditions + await placeholderResendCache.set(messageKey?.id!, true) + await delay(2000) - // Check if message was received or cache was cleared during delay + // Check if message was received during delay if (!(await placeholderResendCache.get(messageKey?.id!))) { logger.debug({ messageKey }, 'message received while resend requested') return 'RESOLVED' @@ -198,15 +191,31 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND } + // Send PDO and get stanzaId (PDO request ID) + const stanzaId = await sendPeerDataOperationMessage(pdoMessage) + + // CRITICAL FIX: Store metadata using stanzaId (not messageKey.id) + // The PDO response will use stanzaId to identify which request it's responding to + if (msgData && stanzaId) { + await placeholderResendCache.set(stanzaId, msgData) + logger.debug( + { messageKey: messageKey.id, stanzaId }, + 'CTWA: Cached metadata using stanzaId for PDO response lookup' + ) + } + + // Clean up message ID marker after storing with stanzaId + await placeholderResendCache.del(messageKey?.id!) + // Cleanup timeout: if no response after 8s, assume phone is offline setTimeout(async () => { - if (await placeholderResendCache.get(messageKey?.id!)) { - logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline') - await placeholderResendCache.del(messageKey?.id!) + if (await placeholderResendCache.get(stanzaId)) { + logger.debug({ stanzaId }, 'PDO message without response after 8 seconds. Phone possibly offline') + await placeholderResendCache.del(stanzaId) } }, 8_000) - return sendPeerDataOperationMessage(pdoMessage) + return stanzaId } // Handles mex newsletter notifications diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 66a3c602..4b3acb3d 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -1,5 +1,6 @@ import type { Readable } from 'stream' import type { URL } from 'url' +import Long from 'long' import { proto } from '../../WAProto/index.js' import type { MediaType } from '../Defaults' import type { BinaryNode } from '../WABinary' @@ -24,6 +25,16 @@ export type WAMessageKey = proto.IMessageKey & { addressingMode?: string isViewOnce?: boolean // TODO: remove out of the message key, place in WebMessageInfo } + +/** Metadata cached for CTWA placeholder resend to preserve original message details */ +export type PlaceholderMessageData = { + key: WAMessageKey + pushName?: string | null + messageTimestamp?: number | Long | null + participant?: string | null + participantAlt?: string | null +} + export type WATextMessage = proto.Message.IExtendedTextMessage export type WAContextInfo = proto.IContextInfo export type WALocationMessage = proto.Message.ILocationMessage diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index 02cd465c..55ca3c7e 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -9,6 +9,7 @@ import type { GroupParticipant, LIDMapping, ParticipantAction, + PlaceholderMessageData, RequestJoinAction, RequestJoinMethod, SignalKeyStoreWithTransaction, @@ -443,15 +444,8 @@ const processMessage = async ( if (response) { // Retrieve cached metadata BEFORE deletion // This preserves original message details that the phone might not send - type CachedMessageData = { - key: WAMessageKey - pushName?: string | null - messageTimestamp?: number | Long | null - participant?: string | null - participantAlt?: string | null - } const cachedData = response.stanzaId - ? await placeholderResendCache?.get(response.stanzaId) + ? await placeholderResendCache?.get(response.stanzaId) : undefined // Clean up cache after retrieving data