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
This commit is contained in:
Claude
2026-02-06 03:07:38 +00:00
parent cd86277bf4
commit bd7c691aa3
+50 -15
View File
@@ -628,11 +628,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} else if (message.templateMessage) { } else if (message.templateMessage) {
return 'template' return 'template'
} else if (message.listMessage) { } else if (message.listMessage) {
// Check if it's a product list (uses PRODUCT_LIST type) // All listMessages (SINGLE_SELECT and PRODUCT_LIST) need biz > list node
// 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
}
return 'list' return 'list'
} else if (message.buttonsResponseMessage) { } else if (message.buttonsResponseMessage) {
return 'buttons_response' return 'buttons_response'
@@ -675,10 +671,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} else if (innerMessage.templateMessage) { } else if (innerMessage.templateMessage) {
return 'template' return 'template'
} else if (innerMessage.listMessage) { } else if (innerMessage.listMessage) {
// Product lists from WhatsApp Business catalog don't need biz node injection // All listMessages (SINGLE_SELECT and PRODUCT_LIST) need biz > list node
if (innerMessage.listMessage.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST) {
return undefined // No biz node needed for catalog product lists
}
return 'list' return 'list'
} else if (innerMessage.buttonsResponseMessage) { } else if (innerMessage.buttonsResponseMessage) {
return 'buttons_response' return 'buttons_response'
@@ -1206,6 +1199,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
'[DEBUG] Interactive message structure' '[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( logger.warn(
{ msgId, buttonType, to: destinationJid, enableInteractiveMessages, isCatalog }, { msgId, buttonType, to: destinationJid, enableInteractiveMessages, isCatalog },
'[EXPERIMENTAL] Injecting biz node for interactive message - may cause ban' '[EXPERIMENTAL] Injecting biz node for interactive message - may cause ban'
@@ -1215,9 +1215,34 @@ export const makeMessagesSocket = (config: SocketConfig) => {
metrics.interactiveMessagesSent.inc({ type: buttonType }) metrics.interactiveMessagesSent.inc({ type: buttonType })
try { try {
// Use nested structure: biz > interactive > native_flow // Differentiate biz node structure based on message type
// All interactive messages (buttons, lists, carousels) use native_flow // isListMessage: true for direct listMessage OR nativeFlowMessage with single_select
// Testing showed that type='list' causes error 479 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' const interactiveType = 'native_flow'
;(stanza.content as BinaryNode[]).push({ ;(stanza.content as BinaryNode[]).push({
tag: 'biz', tag: 'biz',
@@ -1241,11 +1266,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
] ]
}) })
}
// For private 1:1 chats, add bot node (required for some interactive messages to render) // 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 // Only inject for actual user JIDs, not broadcasts, newsletters, or Meta AI bots
// IMPORTANT: Carousels and catalog messages should NOT have bot node // IMPORTANT: Carousels, catalog messages, and list messages should NOT have bot node
// as they are regular interactive messages, not bot messages // 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) // NOTE: Only for regular JIDs, NOT hosted JIDs (to avoid interference with interactive messages)
const isPrivateUserChat = ( const isPrivateUserChat = (
isPnUser(destinationJid) || isPnUser(destinationJid) ||
@@ -1253,7 +1279,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
destinationJid?.endsWith('@c.us') destinationJid?.endsWith('@c.us')
) && !isJidBot(destinationJid) ) && !isJidBot(destinationJid)
if (isPrivateUserChat && !isCarousel && !isCatalog) { if (isPrivateUserChat && !isCarousel && !isCatalog && !isListMessage) {
;(stanza.content as BinaryNode[]).push({ ;(stanza.content as BinaryNode[]).push({
tag: 'bot', tag: 'bot',
attrs: { biz_bot: '1' } attrs: { biz_bot: '1' }
@@ -1272,6 +1298,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
{ msgId, to: destinationJid, buttonType }, { msgId, to: destinationJid, buttonType },
'[EXPERIMENTAL] Skipping bot node for catalog message (with biz node)' '[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 // Track success and latency after message is sent
@@ -1360,6 +1391,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
;(stanza.content as BinaryNode[]).push(...additionalNodes) ;(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`) logger.debug({ msgId }, `sending message to ${participants.length} devices`)
await sendNode(stanza) await sendNode(stanza)