@peopl-health/nexus 1.7.6 → 1.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/basic-usage.js +20 -2
- package/lib/adapters/BaileysProvider.js +3 -3
- package/lib/adapters/TwilioProvider.js +4 -4
- package/lib/controllers/conversationController.js +2 -2
- package/lib/core/MessageProvider.js +1 -1
- package/lib/core/NexusMessaging.js +38 -16
- package/lib/index.js +1 -1
- package/lib/models/messageModel.js +8 -0
- package/lib/storage/MongoStorage.js +9 -5
- package/lib/utils/messageParser.js +62 -6
- package/package.json +1 -1
package/examples/basic-usage.js
CHANGED
|
@@ -19,6 +19,12 @@ async function startServer() {
|
|
|
19
19
|
accountSid: process.env.TWILIO_ACCOUNT_SID,
|
|
20
20
|
authToken: process.env.TWILIO_AUTH_TOKEN,
|
|
21
21
|
whatsappNumber: process.env.TWILIO_WHATSAPP_NUMBER
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Media configuration for AWS S3 upload
|
|
25
|
+
media: {
|
|
26
|
+
bucketName: process.env.AWS_S3_BUCKET_NAME,
|
|
27
|
+
region: process.env.AWS_REGION || 'us-east-1'
|
|
22
28
|
}
|
|
23
29
|
});
|
|
24
30
|
|
|
@@ -28,7 +34,7 @@ async function startServer() {
|
|
|
28
34
|
// Add webhook endpoint for incoming messages
|
|
29
35
|
app.post('/webhook', async (req, res) => {
|
|
30
36
|
try {
|
|
31
|
-
await nexus.
|
|
37
|
+
await nexus.processMessage(req.body);
|
|
32
38
|
res.status(200).send('OK');
|
|
33
39
|
} catch (error) {
|
|
34
40
|
console.error('Webhook error:', error);
|
|
@@ -42,7 +48,12 @@ async function startServer() {
|
|
|
42
48
|
connected: nexus.messaging.isConnected(),
|
|
43
49
|
provider: 'twilio',
|
|
44
50
|
mongodb: nexus.messaging.mongodb?.readyState === 1,
|
|
45
|
-
airtable: !!nexus.messaging.airtable
|
|
51
|
+
airtable: !!nexus.messaging.airtable,
|
|
52
|
+
aws: {
|
|
53
|
+
configured: !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY),
|
|
54
|
+
bucket: process.env.AWS_S3_BUCKET_NAME || 'not configured',
|
|
55
|
+
region: process.env.AWS_REGION || 'us-east-1'
|
|
56
|
+
}
|
|
46
57
|
});
|
|
47
58
|
});
|
|
48
59
|
|
|
@@ -64,7 +75,14 @@ async function startServer() {
|
|
|
64
75
|
console.log('- POST /webhook - Webhook for incoming messages');
|
|
65
76
|
console.log('- GET /status - Connection status');
|
|
66
77
|
console.log('- GET /airtable-test - Test Airtable connection');
|
|
78
|
+
console.log('- GET /media-test - Test AWS media upload configuration');
|
|
67
79
|
console.log('- All Nexus API routes under /api/*');
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log('📸 Media Upload Setup:');
|
|
82
|
+
console.log(' Add these to your .env file for media upload:');
|
|
83
|
+
console.log(' - AWS_ACCESS_KEY_ID=your_access_key');
|
|
84
|
+
console.log(' - AWS_SECRET_ACCESS_KEY=your_secret_key');
|
|
85
|
+
console.log(' - AWS_S3_BUCKET_NAME=your-bucket-name');
|
|
68
86
|
});
|
|
69
87
|
}
|
|
70
88
|
|
|
@@ -84,19 +84,19 @@ class BaileysProvider extends MessageProvider {
|
|
|
84
84
|
throw new Error('Baileys provider not connected');
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const { code,
|
|
87
|
+
const { code, body, fileUrl, fileType, hidePreview = false } = messageData;
|
|
88
88
|
|
|
89
89
|
if (!code) {
|
|
90
90
|
throw new Error('Recipient is required');
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (!
|
|
93
|
+
if (!body && !fileUrl) {
|
|
94
94
|
throw new Error('Message or file URL is required');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const formattedCode = this.formatCode(code);
|
|
98
98
|
let sendOptions = {};
|
|
99
|
-
let formattedMessage = this.formatMessage(
|
|
99
|
+
let formattedMessage = this.formatMessage(body || '');
|
|
100
100
|
|
|
101
101
|
// Handle mentions
|
|
102
102
|
const regex = /@\d+/g;
|
|
@@ -44,7 +44,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
44
44
|
throw new Error('Twilio provider not initialized');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const { code,
|
|
47
|
+
const { code, body, fileUrl, fileType, variables, contentSid } = messageData;
|
|
48
48
|
|
|
49
49
|
const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
|
|
50
50
|
const formattedCode = this.ensureWhatsAppFormat(code);
|
|
@@ -77,8 +77,8 @@ class TwilioProvider extends MessageProvider {
|
|
|
77
77
|
});
|
|
78
78
|
messageParams.contentVariables = JSON.stringify(formattedVariables);
|
|
79
79
|
}
|
|
80
|
-
} else if (
|
|
81
|
-
messageParams.body =
|
|
80
|
+
} else if (body) {
|
|
81
|
+
messageParams.body = body;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Handle media messages
|
|
@@ -314,7 +314,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
314
314
|
mediaType,
|
|
315
315
|
fileName: existingFileName || `${sanitizedBase}.${extension}`,
|
|
316
316
|
fileSize: buffer.length,
|
|
317
|
-
caption: messageData.
|
|
317
|
+
caption: messageData.body || '',
|
|
318
318
|
metadata: {
|
|
319
319
|
originalUrl: fileUrl,
|
|
320
320
|
uploadedAt: new Date().toISOString()
|
|
@@ -260,12 +260,12 @@ const getConversationReplyController = async (req, res) => {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
if (message && message.trim() !== '') {
|
|
263
|
-
messageData.
|
|
263
|
+
messageData.body = message;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
// Handle text message
|
|
267
267
|
else if (message) {
|
|
268
|
-
messageData.
|
|
268
|
+
messageData.body = message;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
console.log('Sending message with data:', JSON.stringify(messageData));
|
|
@@ -28,7 +28,7 @@ class MessageProvider {
|
|
|
28
28
|
* Send a message
|
|
29
29
|
* @param {Object} messageData - Message data
|
|
30
30
|
* @param {string} messageData.code - Recipient
|
|
31
|
-
* @param {string} messageData.
|
|
31
|
+
* @param {string} messageData.body - Message text
|
|
32
32
|
* @param {string} messageData.fileUrl - Optional file URL
|
|
33
33
|
* @param {string} messageData.fileType - File type (text, image, document, audio)
|
|
34
34
|
* @param {Object} messageData.variables - Template variables
|
|
@@ -364,7 +364,7 @@ class NexusMessaging {
|
|
|
364
364
|
|
|
365
365
|
_extractAssistantInputs(messageData) {
|
|
366
366
|
if (!messageData || typeof messageData !== 'object') {
|
|
367
|
-
return { from: null,
|
|
367
|
+
return { from: null, body: null };
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
const from = [
|
|
@@ -380,7 +380,7 @@ class NexusMessaging {
|
|
|
380
380
|
].find((value) => typeof value === 'string' && value.trim().length > 0) || null;
|
|
381
381
|
|
|
382
382
|
const message = [
|
|
383
|
-
typeof messageData.
|
|
383
|
+
typeof messageData.body === 'string' ? messageData.body : null, // Unified body field for all message types
|
|
384
384
|
typeof messageData.message?.conversation === 'string' ? messageData.message.conversation : null,
|
|
385
385
|
typeof messageData.Body === 'string' ? messageData.Body : null,
|
|
386
386
|
typeof messageData.raw?.message?.conversation === 'string' ? messageData.raw.message.conversation : null,
|
|
@@ -392,7 +392,7 @@ class NexusMessaging {
|
|
|
392
392
|
: null
|
|
393
393
|
].find((value) => typeof value === 'string' && value.trim().length > 0) || null;
|
|
394
394
|
|
|
395
|
-
return { from, message };
|
|
395
|
+
return { from, body: message };
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
async handleMessage(messageData) {
|
|
@@ -403,19 +403,24 @@ class NexusMessaging {
|
|
|
403
403
|
|
|
404
404
|
async handleMessageWithAssistant(messageData) {
|
|
405
405
|
try {
|
|
406
|
-
const { from,
|
|
406
|
+
const { from, body } = this._extractAssistantInputs(messageData);
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
// Skip assistant processing for all interactive messages to avoid conflicts with predefined flows
|
|
409
|
+
if (messageData.isInteractive) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!from || !body) {
|
|
409
414
|
console.warn('Unable to resolve assistant inputs from message, skipping automatic reply.');
|
|
410
415
|
return;
|
|
411
416
|
}
|
|
412
417
|
|
|
413
|
-
const response = await replyAssistant(from,
|
|
418
|
+
const response = await replyAssistant(from, body);
|
|
414
419
|
|
|
415
420
|
if (response) {
|
|
416
421
|
await this.sendMessage({
|
|
417
422
|
code: from,
|
|
418
|
-
|
|
423
|
+
body: response,
|
|
419
424
|
processed: true
|
|
420
425
|
});
|
|
421
426
|
}
|
|
@@ -428,7 +433,24 @@ class NexusMessaging {
|
|
|
428
433
|
if (this.messageStorage) {
|
|
429
434
|
await this.messageStorage.saveInteractive(messageData);
|
|
430
435
|
}
|
|
431
|
-
|
|
436
|
+
|
|
437
|
+
this.events.emit('interactive:received', messageData);
|
|
438
|
+
|
|
439
|
+
// Run interactive handler if available
|
|
440
|
+
const result = await this._runPipeline('interactive', messageData, async (ctx) => {
|
|
441
|
+
const handler = this.handlers.onInteractive;
|
|
442
|
+
if (handler) {
|
|
443
|
+
return await handler(ctx, this);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Interactive messages are handled by specific flows on other servers
|
|
447
|
+
// No assistant processing to avoid conflicts with predefined flows
|
|
448
|
+
|
|
449
|
+
return null;
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
this.events.emit('interactive:handled', messageData);
|
|
453
|
+
return result;
|
|
432
454
|
}
|
|
433
455
|
|
|
434
456
|
async handleMedia(messageData) {
|
|
@@ -441,7 +463,7 @@ class NexusMessaging {
|
|
|
441
463
|
|
|
442
464
|
async handleMediaWithAssistant(messageData) {
|
|
443
465
|
try {
|
|
444
|
-
const { from,
|
|
466
|
+
const { from, body } = this._extractAssistantInputs(messageData);
|
|
445
467
|
|
|
446
468
|
if (!from) {
|
|
447
469
|
console.warn('Unable to resolve sender for media message, skipping automatic reply.');
|
|
@@ -457,8 +479,8 @@ class NexusMessaging {
|
|
|
457
479
|
return null;
|
|
458
480
|
})();
|
|
459
481
|
|
|
460
|
-
const fallbackMessage =
|
|
461
|
-
?
|
|
482
|
+
const fallbackMessage = body && body.trim().length > 0
|
|
483
|
+
? body
|
|
462
484
|
: `Media received (${mediaDescriptor || 'attachment'})`;
|
|
463
485
|
|
|
464
486
|
const response = await replyAssistant(from, fallbackMessage);
|
|
@@ -466,7 +488,7 @@ class NexusMessaging {
|
|
|
466
488
|
if (response) {
|
|
467
489
|
await this.sendMessage({
|
|
468
490
|
code: from,
|
|
469
|
-
|
|
491
|
+
body: response
|
|
470
492
|
});
|
|
471
493
|
}
|
|
472
494
|
} catch (error) {
|
|
@@ -508,8 +530,8 @@ class NexusMessaging {
|
|
|
508
530
|
messageData.fileUrl = messageData.fileUrl || mediaPayload.metadata?.presignedUrl || null;
|
|
509
531
|
messageData.fileType = messageData.fileType || mediaPayload.mediaType;
|
|
510
532
|
messageData.caption = messageData.caption || primary.caption;
|
|
511
|
-
if (!messageData.
|
|
512
|
-
messageData.
|
|
533
|
+
if (!messageData.body && messageData.caption) {
|
|
534
|
+
messageData.body = messageData.caption;
|
|
513
535
|
}
|
|
514
536
|
messageData.isMedia = true;
|
|
515
537
|
|
|
@@ -520,7 +542,7 @@ class NexusMessaging {
|
|
|
520
542
|
const messageObj = convertTwilioToInternalFormat(raw);
|
|
521
543
|
const values = getMessageValues(
|
|
522
544
|
messageObj,
|
|
523
|
-
messageData.
|
|
545
|
+
messageData.body || messageData.caption || '',
|
|
524
546
|
null,
|
|
525
547
|
true
|
|
526
548
|
);
|
|
@@ -605,7 +627,7 @@ class NexusMessaging {
|
|
|
605
627
|
if (botResponse && this.provider) {
|
|
606
628
|
await this.provider.sendMessage({
|
|
607
629
|
to: chatId,
|
|
608
|
-
|
|
630
|
+
body: botResponse,
|
|
609
631
|
type: 'text',
|
|
610
632
|
processed: true
|
|
611
633
|
});
|
package/lib/index.js
CHANGED
|
@@ -212,7 +212,7 @@ class Nexus {
|
|
|
212
212
|
* Send a message
|
|
213
213
|
* @param {Object} messageData - Message data
|
|
214
214
|
* @param {string} messageData.code - Recipient phone number
|
|
215
|
-
* @param {string} messageData.
|
|
215
|
+
* @param {string} messageData.body - Message text
|
|
216
216
|
* @param {string} [messageData.fileUrl] - Optional file URL
|
|
217
217
|
* @param {string} [messageData.fileType] - File type
|
|
218
218
|
* @param {Object} [messageData.variables] - Template variables
|
|
@@ -10,6 +10,12 @@ const messageSchema = new mongoose.Schema({
|
|
|
10
10
|
message_id: { type: String, required: true },
|
|
11
11
|
is_group: { type: Boolean, required: true },
|
|
12
12
|
is_media: { type: Boolean, required: true },
|
|
13
|
+
is_interactive: { type: Boolean, default: false },
|
|
14
|
+
interaction_type: {
|
|
15
|
+
type: String,
|
|
16
|
+
enum: ['button', 'list', 'flow', 'quick_reply'],
|
|
17
|
+
default: null
|
|
18
|
+
},
|
|
13
19
|
group_id: { type: String, default: null },
|
|
14
20
|
reply_id: { type: String, default: null },
|
|
15
21
|
processed: { type: Boolean, default: false },
|
|
@@ -64,6 +70,8 @@ async function insertMessage(values) {
|
|
|
64
70
|
message_id: values.message_id,
|
|
65
71
|
is_group: values.is_group,
|
|
66
72
|
is_media: values.is_media,
|
|
73
|
+
is_interactive: values.is_interactive || false,
|
|
74
|
+
interaction_type: values.interaction_type || null,
|
|
67
75
|
group_id: values.group_id,
|
|
68
76
|
reply_id: values.reply_id,
|
|
69
77
|
from_me: values.from_me,
|
|
@@ -21,7 +21,7 @@ class MongoStorage {
|
|
|
21
21
|
const interactionSchema = new mongoose.Schema({
|
|
22
22
|
messageId: String,
|
|
23
23
|
numero: String,
|
|
24
|
-
|
|
24
|
+
interaction_type: String, // 'button', 'list', 'flow'
|
|
25
25
|
payload: mongoose.Schema.Types.Mixed,
|
|
26
26
|
timestamp: String,
|
|
27
27
|
createdAt: { type: Date, default: Date.now }
|
|
@@ -67,7 +67,9 @@ class MongoStorage {
|
|
|
67
67
|
provider: messageData?.provider || 'unknown',
|
|
68
68
|
hasRaw: Boolean(messageData?.raw),
|
|
69
69
|
hasMedia: Boolean(messageData?.media || messageData?.fileUrl),
|
|
70
|
-
hasContentSid: Boolean(messageData?.contentSid)
|
|
70
|
+
hasContentSid: Boolean(messageData?.contentSid),
|
|
71
|
+
is_interactive: messageData.isInteractive,
|
|
72
|
+
interaction_type: messageData.interactionType
|
|
71
73
|
});
|
|
72
74
|
const enrichedMessage = await this._enrichTwilioMedia(messageData);
|
|
73
75
|
const values = this.buildMessageValues(enrichedMessage);
|
|
@@ -135,7 +137,7 @@ class MongoStorage {
|
|
|
135
137
|
fileUrl: undefined,
|
|
136
138
|
fileType: mediaPayload.mediaType || messageData.fileType,
|
|
137
139
|
isMedia: true,
|
|
138
|
-
message: messageData.
|
|
140
|
+
message: messageData.body || rawMessage.Body || primary.caption || '',
|
|
139
141
|
caption: primary.caption || messageData.caption
|
|
140
142
|
};
|
|
141
143
|
} catch (error) {
|
|
@@ -179,7 +181,7 @@ class MongoStorage {
|
|
|
179
181
|
const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName || runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
|
|
180
182
|
|
|
181
183
|
// Use message body directly (template rendering is now handled by the provider)
|
|
182
|
-
let textBody = messageData.
|
|
184
|
+
let textBody = messageData.body;
|
|
183
185
|
|
|
184
186
|
if (!textBody && isMedia) {
|
|
185
187
|
textBody = `[Media:${messageData.fileType || 'attachment'}]`;
|
|
@@ -205,6 +207,8 @@ class MongoStorage {
|
|
|
205
207
|
message_id: providerId,
|
|
206
208
|
is_group: isGroup,
|
|
207
209
|
is_media: isMedia,
|
|
210
|
+
is_interactive: messageData.isInteractive || false,
|
|
211
|
+
interaction_type: messageData.interactionType || null,
|
|
208
212
|
group_id: isGroup ? normalizedNumero : null,
|
|
209
213
|
reply_id: messageData.reply_id || messageData.replyId || null,
|
|
210
214
|
from_me: messageData.fromMe !== undefined ? messageData.fromMe : true,
|
|
@@ -219,7 +223,7 @@ class MongoStorage {
|
|
|
219
223
|
const interaction = new this.schemas.Interaction({
|
|
220
224
|
messageId: interactionData.messageId,
|
|
221
225
|
numero: interactionData.from,
|
|
222
|
-
|
|
226
|
+
interaction_type: interactionData.type, // 'button', 'list', 'flow'
|
|
223
227
|
payload: interactionData.payload,
|
|
224
228
|
timestamp: this.formatTimestamp(interactionData.timestamp)
|
|
225
229
|
});
|
|
@@ -24,20 +24,34 @@ class MessageParser {
|
|
|
24
24
|
|
|
25
25
|
// Check for interactive messages (buttons, lists, flows)
|
|
26
26
|
if (this.isInteractiveMessage(rawMessage)) {
|
|
27
|
+
const interactive = this.parseInteractive(rawMessage);
|
|
28
|
+
const interactionText = this.extractInteractionText(interactive);
|
|
29
|
+
|
|
27
30
|
return {
|
|
28
31
|
...messageData,
|
|
32
|
+
body: interactionText,
|
|
29
33
|
type: 'interactive',
|
|
30
|
-
interactive:
|
|
34
|
+
interactive: interactive,
|
|
35
|
+
isInteractive: true, // Flag to indicate this is an interactive message
|
|
36
|
+
interactionType: interactive.type // Store the specific interaction type
|
|
31
37
|
};
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
// Check for media messages
|
|
35
41
|
if (this.isMediaMessage(rawMessage)) {
|
|
36
|
-
|
|
42
|
+
const mediaResult = {
|
|
37
43
|
...messageData,
|
|
38
44
|
type: 'media',
|
|
39
45
|
media: this.parseMedia(rawMessage)
|
|
40
46
|
};
|
|
47
|
+
|
|
48
|
+
// Check if media message also has text content (caption)
|
|
49
|
+
const textContent = this.extractTextContent(rawMessage);
|
|
50
|
+
if (textContent) {
|
|
51
|
+
mediaResult.body = textContent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return mediaResult;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
// Parse text content
|
|
@@ -46,7 +60,7 @@ class MessageParser {
|
|
|
46
60
|
return { ...messageData, type: 'unknown' };
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
messageData.
|
|
63
|
+
messageData.body = textContent;
|
|
50
64
|
|
|
51
65
|
// Check for commands
|
|
52
66
|
if (this.isCommand(textContent)) {
|
|
@@ -117,12 +131,19 @@ class MessageParser {
|
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
isInteractiveMessage(rawMessage) {
|
|
120
|
-
// Twilio interactive messages
|
|
121
|
-
return !!(
|
|
134
|
+
// Twilio interactive messages - check for any interactive indicators
|
|
135
|
+
return !!(
|
|
136
|
+
rawMessage.ButtonPayload ||
|
|
137
|
+
rawMessage.ListId ||
|
|
138
|
+
rawMessage.FlowData ||
|
|
139
|
+
rawMessage.InteractiveData ||
|
|
140
|
+
rawMessage.ButtonText
|
|
141
|
+
);
|
|
122
142
|
}
|
|
123
143
|
|
|
124
144
|
parseInteractive(rawMessage) {
|
|
125
|
-
if (rawMessage.ButtonPayload) {
|
|
145
|
+
if (rawMessage.ButtonPayload !== undefined && rawMessage.ButtonPayload !== null && rawMessage.ButtonPayload.trim() !== '') {
|
|
146
|
+
// Non-empty payload = regular button
|
|
126
147
|
return {
|
|
127
148
|
type: 'button',
|
|
128
149
|
payload: rawMessage.ButtonPayload,
|
|
@@ -130,6 +151,14 @@ class MessageParser {
|
|
|
130
151
|
};
|
|
131
152
|
}
|
|
132
153
|
|
|
154
|
+
if (rawMessage.ButtonText) {
|
|
155
|
+
return {
|
|
156
|
+
type: 'quick_reply',
|
|
157
|
+
title: rawMessage.ButtonText,
|
|
158
|
+
payload: rawMessage.ButtonText
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
133
162
|
if (rawMessage.ListId) {
|
|
134
163
|
return {
|
|
135
164
|
type: 'list',
|
|
@@ -149,6 +178,33 @@ class MessageParser {
|
|
|
149
178
|
return null;
|
|
150
179
|
}
|
|
151
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Extract interaction text for body field from interactive message
|
|
183
|
+
* @param {Object} interactive - Parsed interactive data
|
|
184
|
+
* @returns {string} Human-readable interaction text
|
|
185
|
+
*/
|
|
186
|
+
extractInteractionText(interactive) {
|
|
187
|
+
if (!interactive) return '';
|
|
188
|
+
|
|
189
|
+
switch (interactive.type) {
|
|
190
|
+
case 'button':
|
|
191
|
+
return interactive.title || interactive.payload || '[Button clicked]';
|
|
192
|
+
|
|
193
|
+
case 'quick_reply':
|
|
194
|
+
return interactive.title || interactive.payload || '[Quick reply]';
|
|
195
|
+
|
|
196
|
+
case 'list':
|
|
197
|
+
return interactive.title || interactive.description || '[List item selected]';
|
|
198
|
+
|
|
199
|
+
case 'flow':
|
|
200
|
+
// Flows contain complex JSON data that doesn't need assistant processing
|
|
201
|
+
return '';
|
|
202
|
+
|
|
203
|
+
default:
|
|
204
|
+
return '[Interactive message]';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
isMediaMessage(rawMessage) {
|
|
153
209
|
// Twilio format
|
|
154
210
|
if (rawMessage.NumMedia && parseInt(rawMessage.NumMedia) > 0) {
|