From 2ed97e68dfbc5be67168d7522b9927fac1316222 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 01:44:47 +0000 Subject: [PATCH] fix(carousel): use viewOnceMessageV2 wrapper with messageContextInfo Switch carousel from direct interactiveMessage to viewOnceMessageV2 wrapper (field 55), confirmed by Z-API as the stable approach for WhatsApp Web rendering from non-Cloud API accounts. Key changes: - Wrap carousel in viewOnceMessageV2 > message > interactiveMessage (V1 caused error 479, direct interactiveMessage didn't render on Web) - Add messageContextInfo with deviceListMetadata and version 2 for multi-device rendering compatibility - Update all helper functions (getButtonType, isCarouselMessage, isCatalogMessage, isListNativeFlow, getButtonArgs) to check V2 path - Update per-device and biz node debug logging for V2 detection https://claude.ai/code/session_018DkDxsjWzM131jy3ivWjZp --- src/Socket/messages-send.ts | 26 +++++++++++++++++--------- src/Utils/messages.ts | 21 +++++++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index d9d65d56..34ca71cd 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -574,7 +574,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Debug: log per-device carousel encoding const carouselInMsg = msgToEncrypt?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage + || msgToEncrypt?.viewOnceMessageV2?.message?.interactiveMessage?.carouselMessage + || msgToEncrypt?.interactiveMessage?.carouselMessage || msgToEncrypt?.deviceSentMessage?.message?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage + || msgToEncrypt?.deviceSentMessage?.message?.viewOnceMessageV2?.message?.interactiveMessage?.carouselMessage + || msgToEncrypt?.deviceSentMessage?.message?.interactiveMessage?.carouselMessage if (carouselInMsg) { try { const debugBytes = proto.Message.encode(proto.Message.fromObject(msgToEncrypt)).finish() @@ -683,9 +687,9 @@ export const makeMessagesSocket = (config: SocketConfig) => { return 'interactive' } - // Check inside viewOnceMessage wrapper (modern nativeFlowMessage format) - // This is the recommended format for interactive messages - const innerMessage = message.viewOnceMessage?.message + // Check inside viewOnceMessage/viewOnceMessageV2 wrapper (modern nativeFlowMessage format) + // V2 is the recommended format for interactive messages (carousel, buttons) + const innerMessage = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message if (innerMessage) { if (innerMessage.buttonsMessage) { return 'buttons' @@ -740,7 +744,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { // For native_flow messages, check for special button types that need specific attributes if (buttonType === 'native_flow') { const interactiveMsg = message.interactiveMessage || - message.viewOnceMessage?.message?.interactiveMessage + message.viewOnceMessage?.message?.interactiveMessage || + message.viewOnceMessageV2?.message?.interactiveMessage if (interactiveMsg?.nativeFlowMessage?.buttons?.[0]) { const firstButtonName = interactiveMsg.nativeFlowMessage.buttons[0].name @@ -770,7 +775,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { */ const isCarouselMessage = (message: proto.IMessage): boolean => { const interactiveMsg = message.interactiveMessage || - message.viewOnceMessage?.message?.interactiveMessage + message.viewOnceMessage?.message?.interactiveMessage || + message.viewOnceMessageV2?.message?.interactiveMessage if (interactiveMsg?.carouselMessage?.cards?.length) { return true @@ -785,7 +791,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { */ const isCatalogMessage = (message: proto.IMessage): boolean => { const interactiveMsg = message.interactiveMessage || - message.viewOnceMessage?.message?.interactiveMessage + message.viewOnceMessage?.message?.interactiveMessage || + message.viewOnceMessageV2?.message?.interactiveMessage const nativeFlow = interactiveMsg?.nativeFlowMessage if (nativeFlow?.buttons?.length) { @@ -806,7 +813,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { */ const isListNativeFlow = (message: proto.IMessage): boolean => { const interactiveMsg = message.interactiveMessage || - message.viewOnceMessage?.message?.interactiveMessage + message.viewOnceMessage?.message?.interactiveMessage || + message.viewOnceMessageV2?.message?.interactiveMessage const nativeFlow = interactiveMsg?.nativeFlowMessage if (nativeFlow?.buttons?.length) { @@ -1284,8 +1292,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { const startTime = Date.now() // Debug: Log message structure to diagnose list detection - const interactiveMsg = message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage - const listMsg = message.listMessage || message.viewOnceMessage?.message?.listMessage + const interactiveMsg = message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage || message.viewOnceMessageV2?.message?.interactiveMessage + const listMsg = message.listMessage || message.viewOnceMessage?.message?.listMessage || message.viewOnceMessageV2?.message?.listMessage const nativeFlowButtons = interactiveMsg?.nativeFlowMessage?.buttons || [] const isListDetected = isListNativeFlow(message) diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 411b03c9..8d57b4ff 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -653,11 +653,20 @@ export const generateCarouselMessage = async ( } } - // Return interactiveMessage directly (NOT wrapped in viewOnceMessage) - // Pastorini's working implementation confirms carousels must be sent - // as direct interactiveMessage, not wrapped in viewOnceMessage + // Wrap in viewOnceMessageV2 (NOT V1!) for WhatsApp Web/Desktop rendering + // V2 (field 55) is the stable wrapper for interactive messages from non-Cloud API accounts + // V1 (field 37) caused error 479 - V2 is the correct approach (confirmed by Z-API) + // messageContextInfo with deviceListMetadata is required for multi-device rendering return { - interactiveMessage + viewOnceMessageV2: { + message: { + messageContextInfo: { + deviceListMetadata: {}, + deviceListMetadataVersion: 2 + }, + interactiveMessage + } + } } } @@ -1154,8 +1163,8 @@ export const generateWAMessageContent = async ( } // Pass options for media processing if cards have images/videos const generated = await generateCarouselMessage(carouselOptions, options) - m.interactiveMessage = generated.interactiveMessage - options.logger?.info('Sending carousel as direct interactiveMessage (plain object, no proto conversion)') + m.viewOnceMessageV2 = generated.viewOnceMessageV2 + options.logger?.info('Sending carousel with viewOnceMessageV2 wrapper (plain object, no proto conversion)') // Return the plain JS object directly WITHOUT calling WAProto.Message.fromObject() // This matches Pastorini's working approach where plain objects are passed directly // to relayMessage. The fromObject() conversion can corrupt nested carousel structures