fix: address lint errors and review feedback
Lint fixes: - Replace `any` types with proper proto interfaces (IInteractiveMessage) - Fix missing blank lines before statements (eslint padding-line-between-statements) - Fix unused variable and indentation in protocol dump - Remove pre-existing `any` casts in createParticipantNodes Review feedback (Copilot/Codex): - Gate protobuf roundtrip test and protocol dump behind debug level (avoids CPU overhead and sensitive data exposure in production) - Add empty array validation for nativeButtons (fixes [].every() edge case) - Support headerTitle in quick_reply buttonsMessage (HeaderType.TEXT) - Fix stale comment that said "wrap viewOnceMessage" when code sends direct Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+29
-66
@@ -552,8 +552,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const meLid = authState.creds.me?.lid
|
const meLid = authState.creds.me?.lid
|
||||||
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
||||||
|
|
||||||
const encryptionPromises = (patchedMessages as any).map(
|
const encryptionPromises = (patchedMessages as { recipientJid: string; message: proto.IMessage }[]).map(
|
||||||
async ({ recipientJid: jid, message: patchedMessage }: any) => {
|
async ({ recipientJid: jid, message: patchedMessage }: { recipientJid: string; message: proto.IMessage }) => {
|
||||||
try {
|
try {
|
||||||
if (!jid) return null
|
if (!jid) return null
|
||||||
|
|
||||||
@@ -647,7 +647,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
// Check if it's a carousel with nativeFlowMessage buttons in cards
|
// Check if it's a carousel with nativeFlowMessage buttons in cards
|
||||||
if (message.interactiveMessage.carouselMessage?.cards?.length) {
|
if (message.interactiveMessage.carouselMessage?.cards?.length) {
|
||||||
const hasNativeFlowButtons = message.interactiveMessage.carouselMessage.cards.some(
|
const hasNativeFlowButtons = message.interactiveMessage.carouselMessage.cards.some(
|
||||||
(card: any) => card?.nativeFlowMessage?.buttons?.length
|
(card: proto.Message.IInteractiveMessage) => card?.nativeFlowMessage?.buttons?.length
|
||||||
)
|
)
|
||||||
if (hasNativeFlowButtons) {
|
if (hasNativeFlowButtons) {
|
||||||
return 'native_flow'
|
return 'native_flow'
|
||||||
@@ -1274,9 +1274,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
let nativeFlowButtons = interactiveMsg?.nativeFlowMessage?.buttons || []
|
let nativeFlowButtons = interactiveMsg?.nativeFlowMessage?.buttons || []
|
||||||
if (nativeFlowButtons.length === 0 && interactiveMsg?.carouselMessage?.cards?.length) {
|
if (nativeFlowButtons.length === 0 && interactiveMsg?.carouselMessage?.cards?.length) {
|
||||||
nativeFlowButtons = interactiveMsg.carouselMessage.cards.flatMap(
|
nativeFlowButtons = interactiveMsg.carouselMessage.cards.flatMap(
|
||||||
(card: any) => card?.nativeFlowMessage?.buttons || []
|
(card: proto.Message.IInteractiveMessage) => card?.nativeFlowMessage?.buttons || []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isListDetected = isListNativeFlow(message)
|
const isListDetected = isListNativeFlow(message)
|
||||||
|
|
||||||
// Log full button details including buttonParamsJson for debugging
|
// Log full button details including buttonParamsJson for debugging
|
||||||
@@ -1501,42 +1502,25 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
|
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
|
||||||
|
|
||||||
// ======= PROTOBUF ROUNDTRIP TEST: Verify encoding preserves carousel =======
|
// ======= PROTOBUF ROUNDTRIP TEST: Verify encoding preserves carousel =======
|
||||||
if (isCarousel) {
|
// Only runs at debug level to avoid performance overhead in production
|
||||||
|
if (isCarousel && logger.level === 'debug') {
|
||||||
try {
|
try {
|
||||||
const encoded = proto.Message.encode(message).finish()
|
const encoded = proto.Message.encode(message).finish()
|
||||||
const decoded = proto.Message.decode(encoded)
|
const decoded = proto.Message.decode(encoded)
|
||||||
// Check both direct and viewOnceMessage-wrapped carousel
|
|
||||||
const decodedInteractive = decoded.interactiveMessage || decoded.viewOnceMessage?.message?.interactiveMessage
|
const decodedInteractive = decoded.interactiveMessage || decoded.viewOnceMessage?.message?.interactiveMessage
|
||||||
const hasCarousel = !!decodedInteractive?.carouselMessage
|
|
||||||
const cardsCount = decodedInteractive?.carouselMessage?.cards?.length || 0
|
const cardsCount = decodedInteractive?.carouselMessage?.cards?.length || 0
|
||||||
const card0 = decodedInteractive?.carouselMessage?.cards?.[0]
|
const card0 = decodedInteractive?.carouselMessage?.cards?.[0]
|
||||||
const card0Header = card0?.header
|
const card0Header = card0?.header
|
||||||
const card0HasImage = !!card0Header?.imageMessage
|
|
||||||
const card0HasThumb = !!(card0Header?.imageMessage as any)?.jpegThumbnail
|
logger.debug(
|
||||||
const card0HasNativeFlow = !!card0?.nativeFlowMessage
|
|
||||||
const card0ButtonCount = card0?.nativeFlowMessage?.buttons?.length || 0
|
|
||||||
logger.info(
|
|
||||||
{
|
{
|
||||||
msgId,
|
msgId,
|
||||||
encodedSize: encoded.length,
|
encodedSize: encoded.length,
|
||||||
hasViewOnceMessage: !!decoded.viewOnceMessage,
|
hasCarouselAfterDecode: !!decodedInteractive?.carouselMessage,
|
||||||
hasInteractiveMessage: !!decodedInteractive,
|
|
||||||
hasCarouselAfterDecode: hasCarousel,
|
|
||||||
cardsCount,
|
cardsCount,
|
||||||
card0: {
|
card0Title: card0Header?.title,
|
||||||
hasHeader: !!card0Header,
|
card0HasImage: !!card0Header?.imageMessage,
|
||||||
title: card0Header?.title,
|
card0Buttons: card0?.nativeFlowMessage?.buttons?.length || 0
|
||||||
subtitle: card0Header?.subtitle,
|
|
||||||
hasMediaAttachment: card0Header?.hasMediaAttachment,
|
|
||||||
hasImageMessage: card0HasImage,
|
|
||||||
hasJpegThumbnail: card0HasThumb,
|
|
||||||
imgHeight: (card0Header?.imageMessage as any)?.height,
|
|
||||||
imgWidth: (card0Header?.imageMessage as any)?.width,
|
|
||||||
hasBody: !!card0?.body?.text,
|
|
||||||
hasFooter: !!card0?.footer?.text,
|
|
||||||
hasNativeFlow: card0HasNativeFlow,
|
|
||||||
buttonCount: card0ButtonCount
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'[ROUNDTRIP] Protobuf encode→decode verification'
|
'[ROUNDTRIP] Protobuf encode→decode verification'
|
||||||
)
|
)
|
||||||
@@ -1546,62 +1530,41 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ======= PROTOCOL INTERCEPTOR: Dump complete stanza for debugging =======
|
// ======= PROTOCOL INTERCEPTOR: Dump complete stanza for debugging =======
|
||||||
if (buttonType || isCarousel) {
|
// Only runs at debug level to avoid logging sensitive content in production
|
||||||
const dumpBinaryNode = (node: any, indent = 0): string => {
|
if ((buttonType || isCarousel) && logger.level === 'debug') {
|
||||||
|
const dumpBinaryNode = (node: BinaryNode, indent = 0): string => {
|
||||||
if (!node) return ''
|
if (!node) return ''
|
||||||
const pad = ' '.repeat(indent)
|
const pad = ' '.repeat(indent)
|
||||||
const tag = node.tag || '?'
|
const tag = node.tag || '?'
|
||||||
const attrs = node.attrs ? Object.entries(node.attrs)
|
const attrEntries = node.attrs
|
||||||
.filter(([_, v]) => v !== undefined && v !== null)
|
? Object.entries(node.attrs).filter(([, v]) => v !== undefined && v !== null)
|
||||||
.map(([k, v]) => `${k}="${v}"`)
|
: []
|
||||||
.join(' ') : ''
|
const attrStr = attrEntries.length > 0
|
||||||
const attrStr = attrs ? ` ${attrs}` : ''
|
? ' ' + attrEntries.map(([k, v]) => `${k}="${v}"`).join(' ')
|
||||||
|
: ''
|
||||||
|
|
||||||
if (!node.content) return `${pad}<${tag}${attrStr}/>`
|
if (!node.content) return `${pad}<${tag}${attrStr}/>`
|
||||||
|
|
||||||
if (Buffer.isBuffer(node.content) || node.content instanceof Uint8Array) {
|
if (Buffer.isBuffer(node.content) || node.content instanceof Uint8Array) {
|
||||||
return `${pad}<${tag}${attrStr}>[binary ${node.content.length} bytes]</${tag}>`
|
return `${pad}<${tag}${attrStr}>[binary ${node.content.length} bytes]</${tag}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(node.content)) {
|
if (Array.isArray(node.content)) {
|
||||||
const children = node.content.map((c: any) => dumpBinaryNode(c, indent + 1)).join('\n')
|
const children = node.content.map((c: BinaryNode) => dumpBinaryNode(c, indent + 1)).join('\n')
|
||||||
return `${pad}<${tag}${attrStr}>\n${children}\n${pad}</${tag}>`
|
return `${pad}<${tag}${attrStr}>\n${children}\n${pad}</${tag}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${pad}<${tag}${attrStr}>${String(node.content).slice(0, 100)}</${tag}>`
|
return `${pad}<${tag}${attrStr}>${String(node.content).slice(0, 100)}</${tag}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump protobuf message structure (which fields are set)
|
logger.debug(
|
||||||
const protoFields: string[] = []
|
|
||||||
const dumpProtoFields = (obj: any, prefix = '') => {
|
|
||||||
if (!obj || typeof obj !== 'object') return
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
if (value === null || value === undefined) continue
|
|
||||||
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
||||||
protoFields.push(`${prefix}${key}: [binary ${(value as any).length}b]`)
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
protoFields.push(`${prefix}${key}: [array ${value.length} items]`)
|
|
||||||
if (value.length > 0 && typeof value[0] === 'object') {
|
|
||||||
dumpProtoFields(value[0], `${prefix}${key}[0].`)
|
|
||||||
}
|
|
||||||
} else if (typeof value === 'object') {
|
|
||||||
protoFields.push(`${prefix}${key}:`)
|
|
||||||
dumpProtoFields(value, `${prefix} `)
|
|
||||||
} else {
|
|
||||||
const strVal = String(value).slice(0, 200)
|
|
||||||
protoFields.push(`${prefix}${key}: ${strVal}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dumpProtoFields(message)
|
|
||||||
|
|
||||||
const stanzaDump = dumpBinaryNode(stanza)
|
|
||||||
logger.info(
|
|
||||||
{
|
{
|
||||||
msgId,
|
msgId,
|
||||||
to: destinationJid,
|
to: destinationJid,
|
||||||
buttonType: buttonType || 'carousel',
|
buttonType: buttonType || 'carousel',
|
||||||
stanzaXML: '\n' + stanzaDump,
|
stanzaXML: '\n' + dumpBinaryNode(stanza)
|
||||||
protobufMessage: '\n' + protoFields.join('\n')
|
|
||||||
},
|
},
|
||||||
'[PROTOCOL-DUMP] Complete stanza and protobuf before send'
|
'[PROTOCOL-DUMP] Stanza structure before send'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// ======= END PROTOCOL INTERCEPTOR =======
|
// ======= END PROTOCOL INTERCEPTOR =======
|
||||||
|
|||||||
+12
-4
@@ -659,6 +659,7 @@ export const generateCarouselMessage = async (
|
|||||||
'[CAROUSEL] imageMessage missing jpegThumbnail - Web may not render'
|
'[CAROUSEL] imageMessage missing jpegThumbnail - Web may not render'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!imageMessage.height || !imageMessage.width) {
|
if (!imageMessage.height || !imageMessage.width) {
|
||||||
mediaOptions.logger?.warn(
|
mediaOptions.logger?.warn(
|
||||||
{ cardTitle: card.title, height: imageMessage.height, width: imageMessage.width },
|
{ cardTitle: card.title, height: imageMessage.height, width: imageMessage.width },
|
||||||
@@ -1198,15 +1199,23 @@ export const generateWAMessageContent = async (
|
|||||||
const nativeMsg = message as any
|
const nativeMsg = message as any
|
||||||
const buttons = nativeMsg.nativeButtons as any[]
|
const buttons = nativeMsg.nativeButtons as any[]
|
||||||
|
|
||||||
|
if (!buttons || buttons.length === 0) {
|
||||||
|
throw new Boom('nativeButtons requires at least one button', { statusCode: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
// Check if ALL buttons are quick_reply (type: 'reply')
|
// Check if ALL buttons are quick_reply (type: 'reply')
|
||||||
const allQuickReply = buttons.every((btn: any) => btn.type === 'reply')
|
const allQuickReply = buttons.every((btn: any) => btn.type === 'reply')
|
||||||
|
|
||||||
if (allQuickReply) {
|
if (allQuickReply) {
|
||||||
// Use legacy buttonsMessage format — works on iOS + Android + Web
|
// Use legacy buttonsMessage format — works on iOS + Android + Web
|
||||||
|
const hasHeaderTitle = !!nativeMsg.headerTitle
|
||||||
const buttonsMessage: proto.Message.IButtonsMessage = {
|
const buttonsMessage: proto.Message.IButtonsMessage = {
|
||||||
contentText: nativeMsg.text || '',
|
contentText: nativeMsg.text || '',
|
||||||
footerText: nativeMsg.footer || undefined,
|
footerText: nativeMsg.footer || undefined,
|
||||||
headerType: proto.Message.ButtonsMessage.HeaderType.EMPTY
|
headerType: hasHeaderTitle
|
||||||
|
? proto.Message.ButtonsMessage.HeaderType.TEXT
|
||||||
|
: proto.Message.ButtonsMessage.HeaderType.EMPTY,
|
||||||
|
...(hasHeaderTitle ? { text: nativeMsg.headerTitle } : {})
|
||||||
}
|
}
|
||||||
buttonsMessage.buttons = buttons.map((btn: any, idx: number) => ({
|
buttonsMessage.buttons = buttons.map((btn: any, idx: number) => ({
|
||||||
buttonId: btn.id || `btn_${idx}`,
|
buttonId: btn.id || `btn_${idx}`,
|
||||||
@@ -1240,9 +1249,8 @@ export const generateWAMessageContent = async (
|
|||||||
}
|
}
|
||||||
// Pass options for media processing if cards have images/videos
|
// Pass options for media processing if cards have images/videos
|
||||||
const generated = await generateCarouselMessage(carouselOptions, options)
|
const generated = await generateCarouselMessage(carouselOptions, options)
|
||||||
// Wrap carousel in viewOnceMessage for Web/Desktop compatibility
|
// Send carousel as direct interactiveMessage (no viewOnceMessage wrapper)
|
||||||
// Same wrapper format as CTA buttons which render correctly on all platforms
|
// viewOnceMessage breaks iOS; messageContextInfo breaks iOS delivery
|
||||||
// Direct interactiveMessage - no viewOnceMessage, no messageContextInfo
|
|
||||||
m.interactiveMessage = generated.interactiveMessage
|
m.interactiveMessage = generated.interactiveMessage
|
||||||
options.logger?.info('Sending carousel as direct interactiveMessage')
|
options.logger?.info('Sending carousel as direct interactiveMessage')
|
||||||
// Return plain JS object - no fromObject() to avoid corrupting nested carousel structures
|
// Return plain JS object - no fromObject() to avoid corrupting nested carousel structures
|
||||||
|
|||||||
Reference in New Issue
Block a user