From bd7c691aa3afc13d6c04bc4e491028d38c3cb3f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 03:07:38 +0000 Subject: [PATCH] fix(list-message): use correct biz node structure for listMessage delivery The listMessage was getting error 405 because the biz node used 'interactive > native_flow' structure which is wrong for list messages. Changed to use 'biz > list (type=product_list, v=2)' structure which matches the Pastorini reference implementation and works on both smartphone and web WhatsApp. Changes: - Differentiate biz node based on message type (list vs interactive) - For listMessage/nativeList: use biz > list (type=product_list, v=2) - For other interactive: keep biz > interactive > native_flow - Skip bot node injection for list messages - All listMessages (SINGLE_SELECT + PRODUCT_LIST) now get biz node - Add diagnostic logging matching Pastorini format https://claude.ai/code/session_01SJdSHiUxtwzV8bb5dedodb --- src/Socket/messages-send.ts | 109 ++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 37 deletions(-) diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index fbf283f2..3a09b9d3 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -628,11 +628,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { } else if (message.templateMessage) { return 'template' } else if (message.listMessage) { - // Check if it's a product list (uses PRODUCT_LIST type) - // Product lists from WhatsApp Business catalog don't need biz node injection - if (message.listMessage.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST) { - return undefined // No biz node needed for catalog product lists - } + // All listMessages (SINGLE_SELECT and PRODUCT_LIST) need biz > list node return 'list' } else if (message.buttonsResponseMessage) { return 'buttons_response' @@ -675,10 +671,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { } else if (innerMessage.templateMessage) { return 'template' } else if (innerMessage.listMessage) { - // Product lists from WhatsApp Business catalog don't need biz node injection - if (innerMessage.listMessage.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST) { - return undefined // No biz node needed for catalog product lists - } + // All listMessages (SINGLE_SELECT and PRODUCT_LIST) need biz > list node return 'list' } else if (innerMessage.buttonsResponseMessage) { return 'buttons_response' @@ -1206,6 +1199,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { '[DEBUG] Interactive message structure' ) + // Log message check for biz node (similar to Pastorini) + const messageKeys = Object.keys(message).filter(k => (message as any)[k] != null) + logger.info( + { hasInteractive: !!interactiveMsg, messageKeys }, + '[BIZ NODE] Checking message for biz node...' + ) + logger.warn( { msgId, buttonType, to: destinationJid, enableInteractiveMessages, isCatalog }, '[EXPERIMENTAL] Injecting biz node for interactive message - may cause ban' @@ -1215,37 +1215,63 @@ export const makeMessagesSocket = (config: SocketConfig) => { metrics.interactiveMessagesSent.inc({ type: buttonType }) try { - // Use nested structure: biz > interactive > native_flow - // All interactive messages (buttons, lists, carousels) use native_flow - // Testing showed that type='list' causes error 479 - const interactiveType = 'native_flow' - ;(stanza.content as BinaryNode[]).push({ - tag: 'biz', - attrs: {}, - content: [ - { - tag: 'interactive', - attrs: { - type: interactiveType, - v: '1' - }, - content: [ - { - tag: interactiveType, - attrs: { - v: '9', - name: 'mixed' - } + // Differentiate biz node structure based on message type + // isListMessage: true for direct listMessage OR nativeFlowMessage with single_select + const isListMessage = buttonType === 'list' || isListNativeFlow(message) + + if (isListMessage) { + // For listMessage: use biz > list (type=product_list, v=2) + // This is the format that works on both phone and web WhatsApp + // Reference: Pastorini implementation + ;(stanza.content as BinaryNode[]).push({ + tag: 'biz', + attrs: {}, + content: [ + { + tag: 'list', + attrs: { + type: 'product_list', + v: '2' } - ] - } - ] - }) + } + ] + }) + logger.info( + { msgId, to: destinationJid }, + '[BIZ NODE] Added list biz node (type=product_list, v=2) for listMessage' + ) + } else { + // For other interactive messages (native_flow, buttons, templates, carousels): + // use biz > interactive > native_flow + const interactiveType = 'native_flow' + ;(stanza.content as BinaryNode[]).push({ + tag: 'biz', + attrs: {}, + content: [ + { + tag: 'interactive', + attrs: { + type: interactiveType, + v: '1' + }, + content: [ + { + tag: interactiveType, + attrs: { + v: '9', + name: 'mixed' + } + } + ] + } + ] + }) + } // For private 1:1 chats, add bot node (required for some interactive messages to render) // Only inject for actual user JIDs, not broadcasts, newsletters, or Meta AI bots - // IMPORTANT: Carousels and catalog messages should NOT have bot node - // as they are regular interactive messages, not bot messages + // IMPORTANT: Carousels, catalog messages, and list messages should NOT have bot node + // List messages use biz > list structure and don't need bot node // NOTE: Only for regular JIDs, NOT hosted JIDs (to avoid interference with interactive messages) const isPrivateUserChat = ( isPnUser(destinationJid) || @@ -1253,7 +1279,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { destinationJid?.endsWith('@c.us') ) && !isJidBot(destinationJid) - if (isPrivateUserChat && !isCarousel && !isCatalog) { + if (isPrivateUserChat && !isCarousel && !isCatalog && !isListMessage) { ;(stanza.content as BinaryNode[]).push({ tag: 'bot', attrs: { biz_bot: '1' } @@ -1272,6 +1298,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { { msgId, to: destinationJid, buttonType }, '[EXPERIMENTAL] Skipping bot node for catalog message (with biz node)' ) + } else if (isListMessage) { + logger.debug( + { msgId, to: destinationJid, buttonType }, + '[BIZ NODE] Skipping bot node for list message (uses list biz node)' + ) } // Track success and latency after message is sent @@ -1360,6 +1391,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { ;(stanza.content as BinaryNode[]).push(...additionalNodes) } + // Log stanza content tags for debugging (similar to Pastorini format) + const contentTags = (stanza.content as BinaryNode[]).map((n: BinaryNode) => n.tag) + logger.info({ msgId, contentTags }, `[STANZA] Content tags: ${JSON.stringify(contentTags)}`) + logger.debug({ msgId }, `sending message to ${participants.length} devices`) await sendNode(stanza)