fix(list-message): convert nativeFlowMessage to direct listMessage for delivery

The viewOnceMessage > interactiveMessage > nativeFlowMessage wrapper with
single_select button was causing error 479 even with correct biz node.
WhatsApp requires the message to be in direct listMessage format (legacy)
paired with biz > list (type=product_list, v=2) node.

This matches the Pastorini implementation which sends:
- messageKeys: ['listMessage'] (NOT viewOnceMessage)
- biz > list (type=product_list, v=2)

The conversion extracts sections from nativeFlowMessage's single_select
buttonParamsJson and creates a proper listMessage with SINGLE_SELECT type.

https://claude.ai/code/session_01SJdSHiUxtwzV8bb5dedodb
This commit is contained in:
Claude
2026-02-06 03:55:38 +00:00
parent bd7c691aa3
commit 191776af67
+55
View File
@@ -829,6 +829,61 @@ export const makeMessagesSocket = (config: SocketConfig) => {
useUserDevicesCache = useUserDevicesCache !== false
useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
// Convert nativeFlowMessage with single_select to direct listMessage (legacy format)
// This is required because WhatsApp expects listMessage format with biz > list node
// The viewOnceMessage > interactiveMessage > nativeFlowMessage wrapper causes error 479
// Reference: Pastorini sends as direct listMessage and it works on phone + web
const innerMsg = message.viewOnceMessage?.message
const nativeFlow = innerMsg?.interactiveMessage?.nativeFlowMessage
if (nativeFlow?.buttons?.length) {
const singleSelectBtn = nativeFlow.buttons.find(
(btn: any) => btn?.name === 'single_select'
)
if (singleSelectBtn?.buttonParamsJson) {
try {
const params = JSON.parse(singleSelectBtn.buttonParamsJson)
const sections = params.sections?.map((section: any) => ({
title: section.title,
rows: section.rows?.map((row: any) => ({
rowId: row.id || row.rowId,
title: row.title,
description: row.description || ''
}))
}))
if (sections?.length) {
// Build direct listMessage (legacy format that works)
const listMessage = proto.Message.ListMessage.fromObject({
title: innerMsg?.interactiveMessage?.header?.title || '',
description: innerMsg?.interactiveMessage?.body?.text || '',
buttonText: params.title || 'Menu',
footerText: innerMsg?.interactiveMessage?.footer?.text || '',
listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
sections
})
// Mutate message in-place: remove viewOnceMessage, add listMessage
delete message.viewOnceMessage
message.listMessage = listMessage
// Keep messageContextInfo if it was nested
if (!message.messageContextInfo && innerMsg?.messageContextInfo) {
message.messageContextInfo = innerMsg.messageContextInfo
}
logger.info(
{ msgId, sectionsCount: sections.length, buttonText: params.title },
'[LIST CONVERT] Converted nativeFlowMessage(single_select) to direct listMessage format'
)
}
} catch (err) {
logger.warn(
{ msgId, error: (err as Error).message },
'[LIST CONVERT] Failed to convert nativeFlowMessage to listMessage, sending as-is'
)
}
}
}
const participants: BinaryNode[] = []
const destinationJid = !isStatus ? finalJid : statusJid
const binaryNodeContent: BinaryNode[] = []