From 89fd500f86ca81aa1dfa6ed54d56748d60002a01 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sat, 21 Feb 2026 02:06:36 -0300 Subject: [PATCH] refactor: clean interactive message logs refactor: clean interactive message logs --- src/Defaults/index.ts | 5 +-- src/Socket/messages-send.ts | 77 +++++++++++++-------------------- src/Types/Message.ts | 4 +- src/Types/Socket.ts | 21 ++------- src/Utils/messages.ts | 13 +++--- src/Utils/prometheus-metrics.ts | 2 +- 6 files changed, 41 insertions(+), 81 deletions(-) diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 3f249add..3b66710b 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -85,10 +85,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { // Enable automatic recovery of Click-to-WhatsApp ads messages // These arrive as "placeholder messages" and need to be requested from the phone enableCTWARecovery: true, - // ⚠️ EXPERIMENTAL: Enable interactive messages (buttons, lists, templates, carousel) - // WARNING: These features may not work and can cause account BANS - // Use ONLY for testing with DISPOSABLE accounts in DEV environment - // Default: true (for dev/testing) - set to false in production + // Enable interactive messages (buttons, lists, templates, carousel) enableInteractiveMessages: true, options: {}, appStateMacVerification: { diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 0cad8af7..7fd9d00d 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -622,9 +622,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return { nodes, shouldIncludeDeviceIdentity } } - // ⚠️ EXPERIMENTAL: Functions to detect and handle interactive messages - // These features may not work and can cause account bans - // Based on Itsukichan/Baileys and baileys_helpers implementation + // Interactive message detection and binary node injection /** * Detects the type of interactive message and returns the appropriate binary node tag @@ -1247,8 +1245,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { content: binaryNodeContent } - // ⚠️ EXPERIMENTAL: Inject 'biz' node for interactive messages - // This may not work and can cause account bans + // Inject 'biz' node for interactive messages const buttonType = getButtonType(message) const isCatalog = isCatalogMessage(message) const isCarousel = isCarouselMessage(message) @@ -1281,29 +1278,20 @@ export const makeMessagesSocket = (config: SocketConfig) => { const isListDetected = isListNativeFlow(message) - // Log full button details including buttonParamsJson for debugging - const buttonDetails = nativeFlowButtons.map((b: any) => ({ - name: b?.name, - paramsJson: b?.buttonParamsJson ? JSON.parse(b.buttonParamsJson) : null - })) - logger.info( { msgId, buttonType, + to: destinationJid, hasListMessage: !!listMsg, hasInteractiveMessage: !!interactiveMsg, hasNativeFlow: !!interactiveMsg?.nativeFlowMessage, nativeFlowButtonNames: nativeFlowButtons.map((b: any) => b?.name), - buttonDetails: JSON.stringify(buttonDetails, null, 2), - isListDetected + isListDetected, + isCatalog, + isCarousel }, - '[DEBUG] Interactive message structure' - ) - - logger.warn( - { msgId, buttonType, to: destinationJid, enableInteractiveMessages, isCatalog }, - '[EXPERIMENTAL] Injecting biz node for interactive message - may cause ban' + '[Interactive] Preparing biz node' ) // Track that we're sending an interactive message @@ -1333,10 +1321,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { } ] }) - logger.info( - { msgId, to: destinationJid }, - '[EXPERIMENTAL] Injected biz node for listMessage (legacy format)' - ) + logger.info({ msgId, to: destinationJid }, '[BIZ NODE] Injected biz > list (product_list, v=2)') } else { const SPECIAL_FLOW_NAMES: Record = { review_and_pay: 'payment_info', @@ -1349,7 +1334,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { logger.info( { msgId, buttonNames: allButtonNames, hasCTA, hasQuickReply, isCTAOnly, nativeFlowName }, - '[EXPERIMENTAL] Determined native_flow name based on button types' + '[BIZ NODE] Injected biz > interactive(native_flow, v=1) > native_flow(v=9, name=' + nativeFlowName + ')' ) const interactiveType = 'native_flow' @@ -1389,38 +1374,26 @@ export const makeMessagesSocket = (config: SocketConfig) => { tag: 'bot', attrs: { biz_bot: '1' } }) - logger.debug( - { msgId, to: destinationJid }, - '[EXPERIMENTAL] Added bot node for private chat interactive message' - ) + logger.info({ msgId, to: destinationJid }, '[BOT NODE] Added bot node (biz_bot=1)') } else if (isNativeFlowButtons) { - logger.debug( - { msgId, to: destinationJid, buttonNames: allButtonNames }, - '[EXPERIMENTAL] Skipping bot node for native_flow buttons (Web/Desktop compatibility)' - ) + logger.debug({ msgId, to: destinationJid }, '[BOT NODE] Skipped — native_flow (Web compatibility)') } else if (isCarousel) { - logger.debug( - { msgId, to: destinationJid, buttonType }, - '[EXPERIMENTAL] Skipping bot node for carousel message' - ) + logger.debug({ msgId, to: destinationJid }, '[BOT NODE] Skipped — carousel message') } else if (isCatalog) { - logger.debug( - { msgId, to: destinationJid, buttonType }, - '[EXPERIMENTAL] Skipping bot node for catalog message (with biz node)' - ) + logger.debug({ msgId, to: destinationJid }, '[BOT NODE] Skipped — catalog message') } // Track success and latency after message is sent metrics.interactiveMessagesSuccess.inc({ type: buttonType }) metrics.interactiveMessagesLatency.observe({ type: buttonType }, Date.now() - startTime) } catch (error) { - logger.error({ error, msgId, buttonType }, '[EXPERIMENTAL] Failed to inject biz node for interactive message') + logger.error({ error, msgId, buttonType }, '[BIZ NODE] Failed to inject biz node') metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'injection_failed' }) } } else if (buttonType && !enableInteractiveMessages) { logger.warn( { msgId, buttonType }, - '[EXPERIMENTAL] Interactive message detected but feature disabled (enableInteractiveMessages=false)' + '[Interactive] Message detected but feature disabled (enableInteractiveMessages=false)' ) metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'feature_disabled' }) } @@ -1493,18 +1466,20 @@ export const makeMessagesSocket = (config: SocketConfig) => { let tcTokenBuffer = existingTokenEntry?.token // Treat expired tokens the same as missing — re-fetch from server - if(tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) { + if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) { logTcToken('expired', { jid: destinationJid, timestamp: existingTokenEntry?.timestamp }) tcTokenBuffer = undefined // Opportunistic cleanup: remove expired token from store try { await authState.keys.set({ tctoken: { [tcTokenJid]: null } }) - } catch { /* ignore cleanup errors */ } + } catch { + /* ignore cleanup errors */ + } } // If tctoken is missing for a 1:1 send, fire-and-forget fetch so the // retry path (error 463 → handleBadAck) can pick it up on resend - if(!tcTokenBuffer?.length && is1on1Send && !tcTokenFetchingJids.has(tcTokenJid)) { + if (!tcTokenBuffer?.length && is1on1Send && !tcTokenFetchingJids.has(tcTokenJid)) { tcTokenFetchingJids.add(tcTokenJid) logTcToken('fetch', { jid: destinationJid }) getPrivacyTokens([destinationJid]) @@ -1525,7 +1500,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { }) } - if(tcTokenBuffer?.length) { + if (tcTokenBuffer?.length) { ;(stanza.content as BinaryNode[]).push({ tag: 'tctoken', attrs: {}, @@ -1544,6 +1519,12 @@ export const makeMessagesSocket = (config: SocketConfig) => { ;(stanza.content as BinaryNode[]).push(...deferredNodes) } + // Log stanza structure for interactive messages + if (buttonType || isCarousel) { + const contentTags = Array.isArray(stanza.content) ? stanza.content.map((n: BinaryNode) => n.tag) : [] + logger.info({ msgId, to: destinationJid, contentTags }, '[STANZA] Content tags: ' + JSON.stringify(contentTags)) + } + logger.debug({ msgId }, `sending message to ${participants.length} devices`) // ======= PROTOBUF ROUNDTRIP TEST: Verify encoding preserves carousel ======= @@ -1615,7 +1596,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Fire-and-forget: issue our token to the contact (like WA Web's sendTcToken) // Only for 1:1 sends where we already have a token, and when bucket boundary crossed - if(is1on1Send && tcTokenBuffer?.length && shouldSendNewTcToken(existingTokenEntry?.senderTimestamp)) { + if (is1on1Send && tcTokenBuffer?.length && shouldSendNewTcToken(existingTokenEntry?.senderTimestamp)) { const issueTimestamp = unixTimestampSeconds() logTcToken('reissue', { jid: destinationJid }) // WA Web writes senderTimestamp only AFTER the IQ succeeds @@ -1635,7 +1616,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Re-read entry to avoid overwriting concurrent notification handler updates const currentData = await authState.keys.get('tctoken', [tcTokenJid]) const currentEntry = currentData[tcTokenJid] - if(currentEntry?.token?.length) { + if (currentEntry?.token?.length) { await authState.keys.set({ tctoken: { [tcTokenJid]: { diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 1df1f129..8cc6192b 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -281,9 +281,7 @@ export type WASendableProduct = Omit { @@ -1328,7 +1327,7 @@ export const generateWAMessageContent = async ( } m.templateMessage = templateMessage - options.logger?.warn('[EXPERIMENTAL] Sending templateMessage - this may not work and can cause bans') + options.logger?.info('[Interactive] Sending templateMessage') } else if (hasNonNullishProperty(message, 'sections')) { // Process list messages - validate limits validateListSections((message as any).sections, (message as any).buttonText) @@ -1350,7 +1349,7 @@ export const generateWAMessageContent = async ( })) m.listMessage = listMessage - options.logger?.warn('[EXPERIMENTAL] Sending listMessage - this may not work and can cause bans') + options.logger?.info({ sections: listMessage.sections?.length || 0 }, '[Interactive] Sending listMessage (sections: ' + (listMessage.sections?.length || 0) + ')') } else if (hasNonNullishProperty(message, 'carousel')) { // Process carousel/interactive messages with viewOnceMessage wrapper const carousel = (message as any).carousel @@ -1530,7 +1529,7 @@ export const generateWAMessageContent = async ( break } } - // ⚠️ EXPERIMENTAL: Process buttons with media (image/video) + // Process buttons with media (image/video) else if ( (hasNonNullishProperty(message, 'image') || hasNonNullishProperty(message, 'video')) && hasNonNullishProperty(message, 'buttons') @@ -1583,7 +1582,7 @@ export const generateWAMessageContent = async ( })) m.buttonsMessage = buttonsMessage - options.logger?.warn('[EXPERIMENTAL] Sending buttonsMessage with media - this may not work and can cause bans') + options.logger?.info('[Interactive] Sending buttonsMessage with media') } else if (hasOptionalProperty(message, 'ptv') && message.ptv) { const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options) m.ptvMessage = videoMessage diff --git a/src/Utils/prometheus-metrics.ts b/src/Utils/prometheus-metrics.ts index 1b632e55..8aafd08a 100644 --- a/src/Utils/prometheus-metrics.ts +++ b/src/Utils/prometheus-metrics.ts @@ -1444,7 +1444,7 @@ export const metrics = { new Counter('ctwa_recovery_failures_total', 'Total CTWA recovery failures', ['reason']) ), - // ========== Interactive Messages Metrics (EXPERIMENTAL) ========== + // ========== Interactive Messages Metrics ========== interactiveMessagesSent: baileysMetrics.register( new Counter( 'interactive_messages_sent_total',