refactor: clean interactive message logs

refactor: clean interactive message logs
This commit is contained in:
Renato Alcara
2026-02-21 02:06:36 -03:00
committed by GitHub
parent 16a0d5b163
commit 89fd500f86
6 changed files with 41 additions and 81 deletions
+1 -4
View File
@@ -85,10 +85,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
// Enable automatic recovery of Click-to-WhatsApp ads messages // Enable automatic recovery of Click-to-WhatsApp ads messages
// These arrive as "placeholder messages" and need to be requested from the phone // These arrive as "placeholder messages" and need to be requested from the phone
enableCTWARecovery: true, enableCTWARecovery: true,
// ⚠️ EXPERIMENTAL: Enable interactive messages (buttons, lists, templates, carousel) // 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
enableInteractiveMessages: true, enableInteractiveMessages: true,
options: {}, options: {},
appStateMacVerification: { appStateMacVerification: {
+24 -43
View File
@@ -622,9 +622,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
return { nodes, shouldIncludeDeviceIdentity } return { nodes, shouldIncludeDeviceIdentity }
} }
// ⚠️ EXPERIMENTAL: Functions to detect and handle interactive messages // Interactive message detection and binary node injection
// These features may not work and can cause account bans
// Based on Itsukichan/Baileys and baileys_helpers implementation
/** /**
* Detects the type of interactive message and returns the appropriate binary node tag * Detects the type of interactive message and returns the appropriate binary node tag
@@ -1247,8 +1245,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
content: binaryNodeContent content: binaryNodeContent
} }
// ⚠️ EXPERIMENTAL: Inject 'biz' node for interactive messages // Inject 'biz' node for interactive messages
// This may not work and can cause account bans
const buttonType = getButtonType(message) const buttonType = getButtonType(message)
const isCatalog = isCatalogMessage(message) const isCatalog = isCatalogMessage(message)
const isCarousel = isCarouselMessage(message) const isCarousel = isCarouselMessage(message)
@@ -1281,29 +1278,20 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const isListDetected = isListNativeFlow(message) 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( logger.info(
{ {
msgId, msgId,
buttonType, buttonType,
to: destinationJid,
hasListMessage: !!listMsg, hasListMessage: !!listMsg,
hasInteractiveMessage: !!interactiveMsg, hasInteractiveMessage: !!interactiveMsg,
hasNativeFlow: !!interactiveMsg?.nativeFlowMessage, hasNativeFlow: !!interactiveMsg?.nativeFlowMessage,
nativeFlowButtonNames: nativeFlowButtons.map((b: any) => b?.name), nativeFlowButtonNames: nativeFlowButtons.map((b: any) => b?.name),
buttonDetails: JSON.stringify(buttonDetails, null, 2), isListDetected,
isListDetected isCatalog,
isCarousel
}, },
'[DEBUG] Interactive message structure' '[Interactive] Preparing biz node'
)
logger.warn(
{ msgId, buttonType, to: destinationJid, enableInteractiveMessages, isCatalog },
'[EXPERIMENTAL] Injecting biz node for interactive message - may cause ban'
) )
// Track that we're sending an interactive message // Track that we're sending an interactive message
@@ -1333,10 +1321,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
] ]
}) })
logger.info( logger.info({ msgId, to: destinationJid }, '[BIZ NODE] Injected biz > list (product_list, v=2)')
{ msgId, to: destinationJid },
'[EXPERIMENTAL] Injected biz node for listMessage (legacy format)'
)
} else { } else {
const SPECIAL_FLOW_NAMES: Record<string, string> = { const SPECIAL_FLOW_NAMES: Record<string, string> = {
review_and_pay: 'payment_info', review_and_pay: 'payment_info',
@@ -1349,7 +1334,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
logger.info( logger.info(
{ msgId, buttonNames: allButtonNames, hasCTA, hasQuickReply, isCTAOnly, nativeFlowName }, { 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' const interactiveType = 'native_flow'
@@ -1389,38 +1374,26 @@ export const makeMessagesSocket = (config: SocketConfig) => {
tag: 'bot', tag: 'bot',
attrs: { biz_bot: '1' } attrs: { biz_bot: '1' }
}) })
logger.debug( logger.info({ msgId, to: destinationJid }, '[BOT NODE] Added bot node (biz_bot=1)')
{ msgId, to: destinationJid },
'[EXPERIMENTAL] Added bot node for private chat interactive message'
)
} else if (isNativeFlowButtons) { } else if (isNativeFlowButtons) {
logger.debug( logger.debug({ msgId, to: destinationJid }, '[BOT NODE] Skipped — native_flow (Web compatibility)')
{ msgId, to: destinationJid, buttonNames: allButtonNames },
'[EXPERIMENTAL] Skipping bot node for native_flow buttons (Web/Desktop compatibility)'
)
} else if (isCarousel) { } else if (isCarousel) {
logger.debug( logger.debug({ msgId, to: destinationJid }, '[BOT NODE] Skipped — carousel message')
{ msgId, to: destinationJid, buttonType },
'[EXPERIMENTAL] Skipping bot node for carousel message'
)
} else if (isCatalog) { } else if (isCatalog) {
logger.debug( logger.debug({ msgId, to: destinationJid }, '[BOT NODE] Skipped — catalog message')
{ msgId, to: destinationJid, buttonType },
'[EXPERIMENTAL] Skipping bot node for catalog message (with biz node)'
)
} }
// Track success and latency after message is sent // Track success and latency after message is sent
metrics.interactiveMessagesSuccess.inc({ type: buttonType }) metrics.interactiveMessagesSuccess.inc({ type: buttonType })
metrics.interactiveMessagesLatency.observe({ type: buttonType }, Date.now() - startTime) metrics.interactiveMessagesLatency.observe({ type: buttonType }, Date.now() - startTime)
} catch (error) { } 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' }) metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'injection_failed' })
} }
} else if (buttonType && !enableInteractiveMessages) { } else if (buttonType && !enableInteractiveMessages) {
logger.warn( logger.warn(
{ msgId, buttonType }, { 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' }) metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'feature_disabled' })
} }
@@ -1499,7 +1472,9 @@ export const makeMessagesSocket = (config: SocketConfig) => {
// Opportunistic cleanup: remove expired token from store // Opportunistic cleanup: remove expired token from store
try { try {
await authState.keys.set({ tctoken: { [tcTokenJid]: null } }) 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 // If tctoken is missing for a 1:1 send, fire-and-forget fetch so the
@@ -1544,6 +1519,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
;(stanza.content as BinaryNode[]).push(...deferredNodes) ;(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`) logger.debug({ msgId }, `sending message to ${participants.length} devices`)
// ======= PROTOBUF ROUNDTRIP TEST: Verify encoding preserves carousel ======= // ======= PROTOBUF ROUNDTRIP TEST: Verify encoding preserves carousel =======
+1 -3
View File
@@ -281,9 +281,7 @@ export type WASendableProduct = Omit<proto.Message.ProductMessage.IProductSnapsh
productImage: WAMediaUpload productImage: WAMediaUpload
} }
// ⚠️ EXPERIMENTAL: Interactive message types // Interactive message types
// These features may not work and can cause account bans
// Use only for testing with disposable accounts
export type ButtonInfo = { export type ButtonInfo = {
buttonId: string buttonId: string
+3 -18
View File
@@ -135,24 +135,9 @@ export type SocketConfig = {
enableCTWARecovery: boolean enableCTWARecovery: boolean
/** /**
* ⚠️ EXPERIMENTAL: Enable interactive messages (buttons, lists, templates, carousel). * Enable interactive messages (buttons, lists, templates, carousel).
* * When true, injects biz/bot binary nodes required by WhatsApp.
* **WARNING**: These features MAY NOT WORK and can cause ACCOUNT BANS. * @default true
*
* WhatsApp actively blocks non-business accounts from sending interactive messages.
* Using this feature can result in:
* - Messages not being delivered
* - Temporary account restrictions
* - Permanent account bans
*
* Use ONLY for:
* - Testing in DEV environment
* - DISPOSABLE test accounts
* - Experimental research
*
* @default true (for dev/testing)
* @see https://github.com/WhiskeySockets/Baileys/issues/56
* @see https://github.com/WhiskeySockets/Baileys/issues/25
*/ */
enableInteractiveMessages: boolean enableInteractiveMessages: boolean
+6 -7
View File
@@ -1287,8 +1287,7 @@ export const generateWAMessageContent = async (
m.viewOnceMessage = generated.viewOnceMessage m.viewOnceMessage = generated.viewOnceMessage
options.logger?.info('Sending productCarouselMessage with viewOnceMessage wrapper') options.logger?.info('Sending productCarouselMessage with viewOnceMessage wrapper')
} }
// ⚠️ EXPERIMENTAL: Check for interactive messages FIRST (buttons, lists, templates) // Interactive messages: buttons, lists, templates
// These use the older API which may not work reliably
else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'buttons')) { else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'buttons')) {
// Process buttons for text messages // Process buttons for text messages
const buttonsMessage: proto.Message.IButtonsMessage = { const buttonsMessage: proto.Message.IButtonsMessage = {
@@ -1304,7 +1303,7 @@ export const generateWAMessageContent = async (
})) }))
m.buttonsMessage = buttonsMessage m.buttonsMessage = buttonsMessage
options.logger?.warn('[EXPERIMENTAL] Sending buttonsMessage - this may not work and can cause bans') options.logger?.info('[Interactive] Sending buttonsMessage (legacy format)')
} else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'templateButtons')) { } else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'templateButtons')) {
// Process templateButtons // Process templateButtons
const hydratedButtons = ((message as any).templateButtons as any[]).map((btn: any) => { const hydratedButtons = ((message as any).templateButtons as any[]).map((btn: any) => {
@@ -1328,7 +1327,7 @@ export const generateWAMessageContent = async (
} }
m.templateMessage = templateMessage 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')) { } else if (hasNonNullishProperty(message, 'sections')) {
// Process list messages - validate limits // Process list messages - validate limits
validateListSections((message as any).sections, (message as any).buttonText) validateListSections((message as any).sections, (message as any).buttonText)
@@ -1350,7 +1349,7 @@ export const generateWAMessageContent = async (
})) }))
m.listMessage = listMessage 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')) { } else if (hasNonNullishProperty(message, 'carousel')) {
// Process carousel/interactive messages with viewOnceMessage wrapper // Process carousel/interactive messages with viewOnceMessage wrapper
const carousel = (message as any).carousel const carousel = (message as any).carousel
@@ -1530,7 +1529,7 @@ export const generateWAMessageContent = async (
break break
} }
} }
// ⚠️ EXPERIMENTAL: Process buttons with media (image/video) // Process buttons with media (image/video)
else if ( else if (
(hasNonNullishProperty(message, 'image') || hasNonNullishProperty(message, 'video')) && (hasNonNullishProperty(message, 'image') || hasNonNullishProperty(message, 'video')) &&
hasNonNullishProperty(message, 'buttons') hasNonNullishProperty(message, 'buttons')
@@ -1583,7 +1582,7 @@ export const generateWAMessageContent = async (
})) }))
m.buttonsMessage = buttonsMessage 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) { } else if (hasOptionalProperty(message, 'ptv') && message.ptv) {
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options) const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options)
m.ptvMessage = videoMessage m.ptvMessage = videoMessage
+1 -1
View File
@@ -1444,7 +1444,7 @@ export const metrics = {
new Counter('ctwa_recovery_failures_total', 'Total CTWA recovery failures', ['reason']) new Counter('ctwa_recovery_failures_total', 'Total CTWA recovery failures', ['reason'])
), ),
// ========== Interactive Messages Metrics (EXPERIMENTAL) ========== // ========== Interactive Messages Metrics ==========
interactiveMessagesSent: baileysMetrics.register( interactiveMessagesSent: baileysMetrics.register(
new Counter( new Counter(
'interactive_messages_sent_total', 'interactive_messages_sent_total',