feat(experimental): add interactive messages support (buttons, lists, templates, carousel)
⚠️ EXPERIMENTAL FEATURE - Use only for testing with disposable accounts Implements full support for WhatsApp interactive messages including: - Simple text buttons (up to 3 buttons) - Buttons with images/videos - List messages (up to 10 items in sections) - Template buttons (quick reply, URL, call actions) - Carousel messages (up to 10 scrollable cards) Features: - Feature flag 'enableInteractiveMessages' (default: true for dev/testing) - Prometheus metrics for tracking sends, successes, failures, and latency - Comprehensive TypeScript types for all interactive message formats - Extensive logging with warnings about potential account bans - Automatic 'biz' node injection when feature is enabled CRITICAL WARNINGS: - These features may NOT work on non-business WhatsApp accounts - Can cause temporary or permanent account BANS - WhatsApp actively blocks this functionality since April 2022 - Messages may be rejected or fail silently - Use ONLY in dev environment with test accounts Architecture: - Added ButtonInfo, Templatable, Listable, Carouselable types to Message.ts - Extended AnyMediaMessageContent and AnyRegularMessageContent - Implemented message generation in messages.ts - Added getButtonType() and getButtonArgs() helpers in messages-send.ts - Injected 'biz' node in stanza construction with metrics tracking - Added 4 new Prometheus metrics: interactiveMessagesSent, Success, Failures, Latency Documentation: - Complete usage guide in INTERACTIVE_MESSAGES.md - Examples for all interactive message types - Metrics monitoring queries - Troubleshooting guide - Migration path to WhatsApp Business API Related issues: - https://github.com/WhiskeySockets/Baileys/issues/56 - https://github.com/WhiskeySockets/Baileys/issues/25 - https://github.com/WhiskeySockets/Baileys/pull/2291 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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**
|
||||||
@@ -82,6 +82,11 @@ 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)
|
||||||
|
// 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: {},
|
options: {},
|
||||||
appStateMacVerification: {
|
appStateMacVerification: {
|
||||||
patch: false,
|
patch: false,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import {
|
|||||||
S_WHATSAPP_NET
|
S_WHATSAPP_NET
|
||||||
} from '../WABinary'
|
} from '../WABinary'
|
||||||
import { USyncQuery, USyncUser } from '../WAUSync'
|
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'
|
import { makeNewsletterSocket } from './newsletter'
|
||||||
|
|
||||||
export const makeMessagesSocket = (config: SocketConfig) => {
|
export const makeMessagesSocket = (config: SocketConfig) => {
|
||||||
@@ -68,7 +68,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
patchMessageBeforeSending,
|
patchMessageBeforeSending,
|
||||||
cachedGroupMetadata,
|
cachedGroupMetadata,
|
||||||
enableRecentMessageCache,
|
enableRecentMessageCache,
|
||||||
maxMsgRetryCount
|
maxMsgRetryCount,
|
||||||
|
enableInteractiveMessages
|
||||||
} = config
|
} = config
|
||||||
const sock = makeNewsletterSocket(config)
|
const sock = makeNewsletterSocket(config)
|
||||||
const {
|
const {
|
||||||
@@ -603,6 +604,33 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
return { nodes, shouldIncludeDeviceIdentity }
|
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 (
|
const relayMessage = async (
|
||||||
jid: string,
|
jid: string,
|
||||||
message: proto.IMessage,
|
message: proto.IMessage,
|
||||||
@@ -965,6 +993,49 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
content: binaryNodeContent
|
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)
|
// if the participant to send to is explicitly specified (generally retry recp)
|
||||||
// ensure the message is only sent to that person
|
// 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
|
// if a retry receipt is sent to everyone -- it'll fail decryption for everyone else who received the msg
|
||||||
|
|||||||
+79
-2
@@ -195,7 +195,7 @@ export type AnyMediaMessageContent = (
|
|||||||
fileName?: string
|
fileName?: string
|
||||||
caption?: string
|
caption?: string
|
||||||
} & Contextable)
|
} & Contextable)
|
||||||
) & { mimetype?: string } & Editable
|
) & { mimetype?: string } & Editable & Partial<Buttonable> & Partial<Templatable>
|
||||||
|
|
||||||
export type ButtonReplyInfo = {
|
export type ButtonReplyInfo = {
|
||||||
displayText: string
|
displayText: string
|
||||||
@@ -215,14 +215,91 @@ export type WASendableProduct = Omit<proto.Message.ProductMessage.IProductSnapsh
|
|||||||
productImage: WAMediaUpload
|
productImage: WAMediaUpload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ EXPERIMENTAL: Interactive message types
|
||||||
|
// These features may not work and can cause account bans
|
||||||
|
// Use only for testing with disposable accounts
|
||||||
|
|
||||||
|
export type ButtonInfo = {
|
||||||
|
buttonId: string
|
||||||
|
buttonText: { displayText: string }
|
||||||
|
type?: proto.Message.ButtonsMessage.Button.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Buttonable = {
|
||||||
|
buttons: ButtonInfo[]
|
||||||
|
headerType?: proto.Message.ButtonsMessage.HeaderType
|
||||||
|
footerText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TemplateButton =
|
||||||
|
| { index: number; quickReplyButton: { displayText: string; id: string } }
|
||||||
|
| { index: number; urlButton: { displayText: string; url: string } }
|
||||||
|
| { index: number; callButton: { displayText: string; phoneNumber: string } }
|
||||||
|
|
||||||
|
export type Templatable = {
|
||||||
|
templateButtons: TemplateButton[]
|
||||||
|
footer?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListSection = {
|
||||||
|
title: string
|
||||||
|
rows: Array<{
|
||||||
|
rowId: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = (
|
export type AnyRegularMessageContent = (
|
||||||
| ({
|
| ({
|
||||||
text: string
|
text: string
|
||||||
linkPreview?: WAUrlInfo | null
|
linkPreview?: WAUrlInfo | null
|
||||||
} & Mentionable &
|
} & Mentionable &
|
||||||
Contextable &
|
Contextable &
|
||||||
Editable)
|
Editable &
|
||||||
|
Partial<Buttonable> &
|
||||||
|
Partial<Templatable> &
|
||||||
|
Partial<Listable>)
|
||||||
| AnyMediaMessageContent
|
| AnyMediaMessageContent
|
||||||
|
| ({
|
||||||
|
interactiveMessage: proto.Message.IInteractiveMessage
|
||||||
|
} & Partial<Carouselable>)
|
||||||
| { event: EventMessageOptions }
|
| { event: EventMessageOptions }
|
||||||
| ({
|
| ({
|
||||||
poll: PollMessageOptions
|
poll: PollMessageOptions
|
||||||
|
|||||||
@@ -133,6 +133,28 @@ export type SocketConfig = {
|
|||||||
*/
|
*/
|
||||||
enableCTWARecovery: boolean
|
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,
|
* Returns if a jid should be ignored,
|
||||||
* no event for that jid will be triggered.
|
* no event for that jid will be triggered.
|
||||||
|
|||||||
@@ -512,6 +512,144 @@ export const generateWAMessageContent = async (
|
|||||||
}
|
}
|
||||||
break
|
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) {
|
} 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
|
||||||
|
|||||||
@@ -1436,6 +1436,20 @@ 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) ==========
|
||||||
|
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 ==========
|
// ========== Media Metrics ==========
|
||||||
mediaUploads: baileysMetrics.register(
|
mediaUploads: baileysMetrics.register(
|
||||||
new Counter('media_uploads_total', 'Total media uploads', ['type', 'status'])
|
new Counter('media_uploads_total', 'Total media uploads', ['type', 'status'])
|
||||||
|
|||||||
Reference in New Issue
Block a user