diff --git a/INTERACTIVE_MESSAGES.md b/INTERACTIVE_MESSAGES.md new file mode 100644 index 00000000..a09c6bb3 --- /dev/null +++ b/INTERACTIVE_MESSAGES.md @@ -0,0 +1,506 @@ +# ⚠️ EXPERIMENTAL: Interactive Messages Guide + +## 🚨 **CRITICAL WARNING** + +**These features MAY NOT WORK and can cause ACCOUNT BANS.** + +- ❌ WhatsApp actively blocks non-business accounts from sending interactive messages +- ❌ Can result in temporary or permanent account bans +- ❌ Messages may not be delivered +- ✅ **Use ONLY for testing with DISPOSABLE accounts in DEV environment** + +## 📋 Table of Contents + +1. [Configuration](#configuration) +2. [Simple Text Buttons](#1-simple-text-buttons) +3. [Buttons with Image](#2-buttons-with-image) +4. [Buttons with Video](#3-buttons-with-video) +5. [List Messages](#4-list-messages) +6. [Template Buttons (Action Buttons)](#5-template-buttons-action-buttons) +7. [Carousel Messages](#6-carousel-messages) +8. [Metrics and Monitoring](#metrics-and-monitoring) + +--- + +## Configuration + +### Enable Interactive Messages + +```typescript +import makeWASocket from '@whiskeysockets/baileys' + +const sock = makeWASocket({ + auth: state, + // ⚠️ Enable experimental interactive messages + // WARNING: Can cause account bans! + enableInteractiveMessages: true, // default: true + logger: pino({ level: 'warn' }) +}) +``` + +### Disable in Production + +```typescript +const sock = makeWASocket({ + auth: state, + // ✅ Disable for production safety + enableInteractiveMessages: false, + logger: pino({ level: 'info' }) +}) +``` + +--- + +## 1. Simple Text Buttons + +Send a message with up to 3 clickable buttons. + +### Example + +```typescript +await sock.sendMessage(jid, { + text: 'Choose an option:', + buttons: [ + { + buttonId: 'btn1', + buttonText: { displayText: 'Option 1' } + }, + { + buttonId: 'btn2', + buttonText: { displayText: 'Option 2' } + }, + { + buttonId: 'btn3', + buttonText: { displayText: 'Option 3' } + } + ], + footerText: 'Optional footer text' +}) +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `text` | string | ✅ | Main message text | +| `buttons` | ButtonInfo[] | ✅ | Array of buttons (max 3) | +| `footerText` | string | ❌ | Footer text below buttons | + +--- + +## 2. Buttons with Image + +Send an image with interactive buttons. + +### Example + +```typescript +await sock.sendMessage(jid, { + image: { url: 'https://example.com/image.jpg' }, + caption: 'Image description', + buttons: [ + { + buttonId: 'view_more', + buttonText: { displayText: 'View More' } + }, + { + buttonId: 'buy_now', + buttonText: { displayText: 'Buy Now' } + } + ], + footerText: 'Available now' +}) +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `image` | WAMediaUpload | ✅ | Image URL or Buffer | +| `caption` | string | ❌ | Image caption | +| `buttons` | ButtonInfo[] | ✅ | Array of buttons (max 3) | +| `footerText` | string | ❌ | Footer text | + +--- + +## 3. Buttons with Video + +Send a video with interactive buttons. + +### Example + +```typescript +await sock.sendMessage(jid, { + video: { url: 'https://example.com/video.mp4' }, + caption: 'Watch this!', + buttons: [ + { + buttonId: 'watch', + buttonText: { displayText: 'Watch Now' } + } + ], + footerText: 'New release' +}) +``` + +--- + +## 4. List Messages + +Send a message with a menu of up to 10 options organized in sections. + +### Example + +```typescript +await sock.sendMessage(jid, { + text: 'Choose a product:', + title: 'Product Catalog', + buttonText: 'View Options', + sections: [ + { + title: 'Electronics', + rows: [ + { + rowId: 'laptop_1', + title: 'Laptop Pro', + description: '$999 - High performance' + }, + { + rowId: 'phone_1', + title: 'Smartphone X', + description: '$699 - Latest model' + } + ] + }, + { + title: 'Accessories', + rows: [ + { + rowId: 'case_1', + title: 'Phone Case', + description: '$29 - Protective' + } + ] + } + ], + footerText: 'Free shipping' +}) +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `text` | string | ✅ | Message description | +| `title` | string | ❌ | List title | +| `buttonText` | string | ✅ | Text on button that opens list | +| `sections` | ListSection[] | ✅ | Array of sections (max 10 items total) | +| `footerText` | string | ❌ | Footer text | + +### Limits + +- Maximum 10 sections +- Maximum 10 rows total across all sections +- Row title: 24 characters max +- Row description: 72 characters max + +--- + +## 5. Template Buttons (Action Buttons) + +Send buttons with actions: Quick Reply, URL, or Call. + +### Example + +```typescript +await sock.sendMessage(jid, { + text: 'Contact us or visit our website', + templateButtons: [ + // Quick Reply Button + { + index: 1, + quickReplyButton: { + displayText: 'Quick Reply', + id: 'reply_1' + } + }, + // URL Button + { + index: 2, + urlButton: { + displayText: 'Visit Website', + url: 'https://example.com' + } + }, + // Call Button + { + index: 3, + callButton: { + displayText: 'Call Us', + phoneNumber: '+551199999999' + } + } + ], + footer: 'We are here to help' +}) +``` + +### Button Types + +| Type | Description | Parameters | +|------|-------------|------------| +| `quickReplyButton` | Quick response button | `displayText`, `id` | +| `urlButton` | Opens URL in browser | `displayText`, `url` | +| `callButton` | Initiates phone call | `displayText`, `phoneNumber` | + +--- + +## 6. Carousel Messages + +Send up to 10 scrollable cards with images/videos and buttons. + +### Example + +```typescript +await sock.sendMessage(jid, { + text: 'Check out our products', + carousel: { + cards: [ + // Card 1 + { + header: { + title: 'Product 1', + imageMessage: { + url: 'https://example.com/product1.jpg', + mimetype: 'image/jpeg' + }, + hasMediaAttachment: true + }, + body: { text: 'Amazing product - $99.90' }, + footer: { text: 'Limited stock' }, + nativeFlowMessage: { + buttons: [ + { + name: 'quick_reply', + buttonParamsJson: JSON.stringify({ + display_text: 'Buy Now', + id: 'buy_1' + }) + }, + { + name: 'cta_url', + buttonParamsJson: JSON.stringify({ + display_text: 'More Info', + url: 'https://example.com/product1' + }) + } + ] + } + }, + // Card 2 + { + header: { + title: 'Product 2', + imageMessage: { + url: 'https://example.com/product2.jpg', + mimetype: 'image/jpeg' + }, + hasMediaAttachment: true + }, + body: { text: 'Great deal - $149.90' }, + footer: { text: 'Best seller' }, + nativeFlowMessage: { + buttons: [ + { + name: 'quick_reply', + buttonParamsJson: JSON.stringify({ + display_text: 'Buy Now', + id: 'buy_2' + }) + } + ] + } + } + ], + messageVersion: 1 + } +}) +``` + +### Limits + +- Maximum 10 cards per carousel +- Each card requires an image or video +- Up to 3 buttons per card +- All cards must have same media type (all images or all videos) + +--- + +## Metrics and Monitoring + +All interactive messages are tracked with Prometheus metrics: + +### Available Metrics + +```typescript +// Messages sent by type +metrics.interactiveMessagesSent.inc({ type: 'buttons' }) + +// Successful sends +metrics.interactiveMessagesSuccess.inc({ type: 'list' }) + +// Failures with reason +metrics.interactiveMessagesFailures.inc({ + type: 'template', + reason: 'feature_disabled' +}) + +// Send latency +metrics.interactiveMessagesLatency.observe({ type: 'carousel' }, 1234) +``` + +### Monitoring Dashboard + +Query these metrics in Prometheus/Grafana: + +```promql +# Total interactive messages sent +sum(interactive_messages_sent_total) by (type) + +# Success rate +sum(interactive_messages_success_total) / sum(interactive_messages_sent_total) + +# Failure breakdown +sum(interactive_messages_failures_total) by (type, reason) + +# P95 latency +histogram_quantile(0.95, interactive_messages_latency_ms) +``` + +--- + +## Handling Responses + +### Button Response + +```typescript +sock.ev.on('messages.upsert', async ({ messages }) => { + const msg = messages[0] + + // Button response + if (msg.message?.buttonsResponseMessage) { + const buttonId = msg.message.buttonsResponseMessage.selectedButtonId + const displayText = msg.message.buttonsResponseMessage.selectedDisplayText + + console.log(`User clicked: ${buttonId} - "${displayText}"`) + } + + // Template button response + if (msg.message?.templateButtonReplyMessage) { + const selectedId = msg.message.templateButtonReplyMessage.selectedId + const selectedText = msg.message.templateButtonReplyMessage.selectedDisplayText + + console.log(`User clicked template: ${selectedId} - "${selectedText}"`) + } + + // List response + if (msg.message?.listResponseMessage) { + const rowId = msg.message.listResponseMessage.singleSelectReply.selectedRowId + const title = msg.message.listResponseMessage.title + + console.log(`User selected from list: ${rowId} - "${title}"`) + } +}) +``` + +--- + +## Troubleshooting + +### Message Not Showing + +- ✅ Verify `enableInteractiveMessages` is `true` +- ✅ Check logs for `[EXPERIMENTAL]` warnings +- ✅ Ensure you're using a test/disposable account +- ❌ Interactive messages don't work on production accounts + +### Account Banned/Restricted + +- ⚠️ This is expected behavior +- ⚠️ WhatsApp blocks non-business accounts +- ✅ Create a new test account +- ✅ Use official WhatsApp Business API for production + +### Metrics Not Showing + +Check feature flag: +```typescript +if (buttonType && !enableInteractiveMessages) { + // Metrics will show reason: 'feature_disabled' +} +``` + +--- + +## Migration to WhatsApp Business API + +For production use, migrate to official API: + +### Evolution API (Cloud Mode) + +```typescript +// config.ts +export const evolutionConfig = { + apiUrl: 'https://your-evolution-api.com', + apiKey: 'your-api-key', + instance: 'your-instance' +} + +// Send button via Evolution API +await fetch(`${evolutionConfig.apiUrl}/message/sendButton`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': evolutionConfig.apiKey + }, + body: JSON.stringify({ + number: '5511999999999', + options: { + delay: 1200, + presence: 'composing' + }, + buttonMessage: { + text: 'Choose an option', + buttons: [ + { id: '1', text: 'Option 1' }, + { id: '2', text: 'Option 2' } + ], + footer: 'Footer text' + } + }) +}) +``` + +--- + +## References + +- [WhatsApp Official Business API](https://business.whatsapp.com/products/business-platform) +- [Evolution API Documentation](https://doc.evolution-api.com/) +- [Baileys GitHub Issues #56](https://github.com/WhiskeySockets/Baileys/issues/56) +- [Baileys GitHub Issues #25](https://github.com/WhiskeySockets/Baileys/issues/25) + +--- + +## ⚠️ Final Reminder + +**THESE FEATURES ARE EXPERIMENTAL AND UNRELIABLE** + +- Use only for testing +- Expect failures and bans +- Not suitable for production +- Official WhatsApp Business API is the only supported way + +--- + +**Generated by rsalcara/InfiniteAPI** diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index c2cb2c37..e8d3395c 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -82,6 +82,11 @@ 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 + enableInteractiveMessages: true, options: {}, appStateMacVerification: { patch: false, diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index ae2be48d..9660b40b 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -56,7 +56,7 @@ import { S_WHATSAPP_NET } from '../WABinary' import { USyncQuery, USyncUser } from '../WAUSync' -import { recordMessageSent, recordMessageRetry, recordMessageFailure } from '../Utils/prometheus-metrics' +import { recordMessageSent, recordMessageRetry, recordMessageFailure, metrics } from '../Utils/prometheus-metrics' import { makeNewsletterSocket } from './newsletter' export const makeMessagesSocket = (config: SocketConfig) => { @@ -68,7 +68,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, - maxMsgRetryCount + maxMsgRetryCount, + enableInteractiveMessages } = config const sock = makeNewsletterSocket(config) const { @@ -603,6 +604,33 @@ 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 + const getButtonType = (message: proto.IMessage): string | undefined => { + if (message.buttonsMessage) { + return 'buttons' + } else if (message.templateMessage) { + return 'template' + } else if (message.listMessage) { + return 'list' + } else if (message.buttonsResponseMessage) { + return 'buttons_response' + } else if (message.listResponseMessage) { + return 'list_response' + } else if (message.templateButtonReplyMessage) { + return 'template_reply' + } else if (message.interactiveMessage) { + return 'interactive' + } + return undefined + } + + const getButtonArgs = (message: proto.IMessage): BinaryNodeAttributes => { + // Return appropriate attributes for each button type + // This is often empty but required for the 'biz' node structure + return {} + } + const relayMessage = async ( jid: string, message: proto.IMessage, @@ -965,6 +993,49 @@ export const makeMessagesSocket = (config: SocketConfig) => { content: binaryNodeContent } + // ⚠️ EXPERIMENTAL: Inject 'biz' node for interactive messages + // This may not work and can cause account bans + const buttonType = getButtonType(message) + if (buttonType && enableInteractiveMessages) { + const startTime = Date.now() + logger.warn( + { msgId, buttonType, to: destinationJid, enableInteractiveMessages }, + '[EXPERIMENTAL] Injecting biz node for interactive message - may cause ban' + ) + + // Track that we're sending an interactive message + metrics.interactiveMessagesSent.inc({ type: buttonType }) + + try { + ;(stanza.content as BinaryNode[]).push({ + tag: 'biz', + attrs: {}, + content: [ + { + tag: buttonType, + attrs: getButtonArgs(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' + ) + 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)' + ) + metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'feature_disabled' }) + } + // if the participant to send to is explicitly specified (generally retry recp) // ensure the message is only sent to that person // if a retry receipt is sent to everyone -- it'll fail decryption for everyone else who received the msg diff --git a/src/Types/Message.ts b/src/Types/Message.ts index dc8d7dfd..d1ac8a68 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -195,7 +195,7 @@ export type AnyMediaMessageContent = ( fileName?: string caption?: string } & Contextable) -) & { mimetype?: string } & Editable +) & { mimetype?: string } & Editable & Partial & Partial export type ButtonReplyInfo = { displayText: string @@ -215,14 +215,91 @@ export type WASendableProduct = Omit +} + +export type Listable = { + sections: ListSection[] + title?: string + buttonText?: string +} + +export type CarouselCard = { + header: { + title: string + imageMessage?: { + url: string + mimetype: string + } + videoMessage?: { + url: string + mimetype: string + } + hasMediaAttachment: boolean + } + body: { text: string } + footer?: { text: string } + nativeFlowMessage?: { + buttons: Array<{ + name: string + buttonParamsJson: string + }> + } +} + +export type Carouselable = { + carousel: { + cards: CarouselCard[] + messageVersion?: number + } +} + export type AnyRegularMessageContent = ( | ({ text: string linkPreview?: WAUrlInfo | null } & Mentionable & Contextable & - Editable) + Editable & + Partial & + Partial & + Partial) | AnyMediaMessageContent + | ({ + interactiveMessage: proto.Message.IInteractiveMessage + } & Partial) | { event: EventMessageOptions } | ({ poll: PollMessageOptions diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index 5b0d460c..cd4dfb43 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -133,6 +133,28 @@ export type SocketConfig = { */ enableCTWARecovery: boolean + /** + * ⚠️ EXPERIMENTAL: Enable interactive messages (buttons, lists, templates, carousel). + * + * **WARNING**: These features MAY NOT WORK and can cause ACCOUNT BANS. + * + * 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 + /** * Returns if a jid should be ignored, * no event for that jid will be triggered. diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index c310e834..24235ca9 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -512,6 +512,144 @@ export const generateWAMessageContent = async ( } break } + } + // ⚠️ EXPERIMENTAL: Interactive messages - may not work and can cause account bans + // Process buttons for text messages + else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'buttons')) { + const buttonsMessage: proto.Message.IButtonsMessage = { + contentText: (message as any).text, + footerText: (message as any).footerText, + headerType: (message as any).headerType || proto.Message.ButtonsMessage.HeaderType.EMPTY + } + + // Add buttons + buttonsMessage.buttons = ((message as any).buttons as any[]).map((btn: any, idx: number) => ({ + buttonId: btn.buttonId || `btn_${idx}`, + buttonText: { displayText: btn.buttonText?.displayText || btn.displayText || btn.text }, + type: btn.type || proto.Message.ButtonsMessage.Button.Type.RESPONSE + })) + + m.buttonsMessage = buttonsMessage + options.logger?.warn('[EXPERIMENTAL] Sending buttonsMessage - this may not work and can cause bans') + } + // Process templateButtons + else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'templateButtons')) { + const templateMessage: proto.Message.ITemplateMessage = { + hydratedTemplate: { + hydratedContentText: (message as any).text, + hydratedFooterText: (message as any).footer + } + } + + // Add template buttons + templateMessage.hydratedTemplate!.hydratedButtons = ((message as any).templateButtons as any[]).map((btn: any) => { + if (btn.quickReplyButton) { + return { + index: btn.index, + quickReplyButton: btn.quickReplyButton + } + } else if (btn.urlButton) { + return { + index: btn.index, + urlButton: btn.urlButton + } + } else if (btn.callButton) { + return { + index: btn.index, + callButton: btn.callButton + } + } + return btn + }) + + m.templateMessage = templateMessage + options.logger?.warn('[EXPERIMENTAL] Sending templateMessage - this may not work and can cause bans') + } + // Process list messages + else if (hasNonNullishProperty(message, 'sections')) { + const listMessage: proto.Message.IListMessage = { + title: (message as any).title, + description: (message as any).text, + buttonText: (message as any).buttonText || 'View options', + footerText: (message as any).footerText, + listType: proto.Message.ListMessage.ListType.SINGLE_SELECT + } + + // Add sections + listMessage.sections = ((message as any).sections as any[]).map((section: any) => ({ + title: section.title, + rows: section.rows.map((row: any) => ({ + rowId: row.rowId || row.id, + title: row.title, + description: row.description + })) + })) + + m.listMessage = listMessage + options.logger?.warn('[EXPERIMENTAL] Sending listMessage - this may not work and can cause bans') + } + // Process carousel/interactive messages + else if (hasNonNullishProperty(message, 'carousel')) { + const carousel = (message as any).carousel + const interactiveMessage: proto.Message.IInteractiveMessage = { + header: carousel.header || { + title: carousel.title || 'Carousel', + hasMediaAttachment: false + }, + body: { + text: (message as any).text || carousel.description || '' + }, + footer: carousel.footer ? { text: carousel.footer } : undefined, + carouselMessage: { + cards: carousel.cards.map((card: any) => ({ + header: card.header, + body: card.body, + footer: card.footer, + nativeFlowMessage: card.nativeFlowMessage + })), + messageVersion: carousel.messageVersion || 1 + } + } + + m.interactiveMessage = interactiveMessage + options.logger?.warn('[EXPERIMENTAL] Sending carouselMessage - this may not work and can cause bans') + } + // Process buttons with media (image/video) + else if ( + (hasNonNullishProperty(message, 'image') || hasNonNullishProperty(message, 'video')) && + hasNonNullishProperty(message, 'buttons') + ) { + const mediaType = hasNonNullishProperty(message, 'image') ? 'image' : 'video' + const headerType = + mediaType === 'image' + ? proto.Message.ButtonsMessage.HeaderType.IMAGE + : proto.Message.ButtonsMessage.HeaderType.VIDEO + + // Prepare media + const mediaMessage = await prepareWAMessageMedia(message as any, options) + + const buttonsMessage: proto.Message.IButtonsMessage = { + contentText: (message as any).caption || (message as any).text || '', + footerText: (message as any).footerText, + headerType: headerType + } + + // Add media + if (mediaType === 'image') { + buttonsMessage.imageMessage = mediaMessage.imageMessage + } else { + buttonsMessage.videoMessage = mediaMessage.videoMessage + } + + // Add buttons + buttonsMessage.buttons = ((message as any).buttons as any[]).map((btn: any, idx: number) => ({ + buttonId: btn.buttonId || `btn_${idx}`, + buttonText: { displayText: btn.buttonText?.displayText || btn.displayText || btn.text }, + type: btn.type || proto.Message.ButtonsMessage.Button.Type.RESPONSE + })) + + m.buttonsMessage = buttonsMessage + options.logger?.warn('[EXPERIMENTAL] Sending buttonsMessage with media - this may not work and can cause bans') } 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 c24a43fe..cabeb59b 100644 --- a/src/Utils/prometheus-metrics.ts +++ b/src/Utils/prometheus-metrics.ts @@ -1436,6 +1436,20 @@ export const metrics = { new Counter('ctwa_recovery_failures_total', 'Total CTWA recovery failures', ['reason']) ), + // ========== Interactive Messages Metrics (EXPERIMENTAL) ========== + interactiveMessagesSent: baileysMetrics.register( + new Counter('interactive_messages_sent_total', 'Total interactive messages sent (buttons/lists/templates/carousel)', ['type']) + ), + interactiveMessagesSuccess: baileysMetrics.register( + new Counter('interactive_messages_success_total', 'Total interactive messages successfully sent', ['type']) + ), + interactiveMessagesFailures: baileysMetrics.register( + new Counter('interactive_messages_failures_total', 'Total interactive message send failures', ['type', 'reason']) + ), + interactiveMessagesLatency: baileysMetrics.register( + new Histogram('interactive_messages_latency_ms', 'Interactive message send latency in ms', ['type'], [100, 250, 500, 1000, 2500, 5000, 10000]) + ), + // ========== Media Metrics ========== mediaUploads: baileysMetrics.register( new Counter('media_uploads_total', 'Total media uploads', ['type', 'status'])