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:
+72
-37
@@ -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,37 +1215,63 @@ 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)
|
||||||
const interactiveType = 'native_flow'
|
|
||||||
;(stanza.content as BinaryNode[]).push({
|
if (isListMessage) {
|
||||||
tag: 'biz',
|
// For listMessage: use biz > list (type=product_list, v=2)
|
||||||
attrs: {},
|
// This is the format that works on both phone and web WhatsApp
|
||||||
content: [
|
// Reference: Pastorini implementation
|
||||||
{
|
;(stanza.content as BinaryNode[]).push({
|
||||||
tag: 'interactive',
|
tag: 'biz',
|
||||||
attrs: {
|
attrs: {},
|
||||||
type: interactiveType,
|
content: [
|
||||||
v: '1'
|
{
|
||||||
},
|
tag: 'list',
|
||||||
content: [
|
attrs: {
|
||||||
{
|
type: 'product_list',
|
||||||
tag: interactiveType,
|
v: '2'
|
||||||
attrs: {
|
|
||||||
v: '9',
|
|
||||||
name: 'mixed'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
]
|
})
|
||||||
})
|
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)
|
// 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user