@peopl-health/nexus 1.7.5 → 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 +67 -6
- package/lib/controllers/conversationController.js +2 -2
- package/lib/core/MessageProvider.js +1 -1
- package/lib/core/NexusMessaging.js +41 -17
- 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 +63 -7
- 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
|
|
@@ -154,6 +154,65 @@ class TwilioProvider extends MessageProvider {
|
|
|
154
154
|
hasContentSid: Boolean(scheduledMessage.contentSid)
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
const updateStatus = async (status, messageId = null, error = null) => {
|
|
158
|
+
if (!scheduledMessage) return;
|
|
159
|
+
|
|
160
|
+
const now = new Date();
|
|
161
|
+
const errorCode = error?.code || error?.status || error?.errorCode || null;
|
|
162
|
+
const errorMessage = error?.message || error?.statusMessage || null;
|
|
163
|
+
const statusEntry = { status, at: now, errorCode, errorMessage };
|
|
164
|
+
const baseUpdate = {
|
|
165
|
+
status,
|
|
166
|
+
lastStatus: status,
|
|
167
|
+
lastStatusAt: now,
|
|
168
|
+
errorCode,
|
|
169
|
+
errorMessage
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (messageId) baseUpdate.wa_id = messageId;
|
|
173
|
+
|
|
174
|
+
const ScheduledMessageModel = (() => {
|
|
175
|
+
if (scheduledMessage.constructor && typeof scheduledMessage.constructor.updateOne === 'function') {
|
|
176
|
+
return scheduledMessage.constructor;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
return require('../models/agendaMessageModel').ScheduledMessage;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
184
|
+
|
|
185
|
+
if (!ScheduledMessageModel) {
|
|
186
|
+
console.warn('[TwilioProvider] Scheduled message model unavailable for status update');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const query = (() => {
|
|
191
|
+
if (scheduledMessage._id) return { _id: scheduledMessage._id };
|
|
192
|
+
if (messageId) return { wa_id: messageId };
|
|
193
|
+
if (scheduledMessage.wa_id) return { wa_id: scheduledMessage.wa_id };
|
|
194
|
+
return null;
|
|
195
|
+
})();
|
|
196
|
+
|
|
197
|
+
if (!query) {
|
|
198
|
+
console.warn('[TwilioProvider] Scheduled message status update skipped: no identifier', {
|
|
199
|
+
hasId: Boolean(scheduledMessage._id),
|
|
200
|
+
messageId,
|
|
201
|
+
existingWaId: scheduledMessage.wa_id
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await ScheduledMessageModel.updateOne(query, {
|
|
208
|
+
$set: baseUpdate,
|
|
209
|
+
$push: { statusHistory: statusEntry }
|
|
210
|
+
});
|
|
211
|
+
} catch (statusErr) {
|
|
212
|
+
console.warn('[TwilioProvider] Failed to update scheduled message status', statusErr?.message || statusErr);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
157
216
|
setTimeout(async () => {
|
|
158
217
|
try {
|
|
159
218
|
// Convert Mongoose document to plain object if needed
|
|
@@ -164,9 +223,11 @@ class TwilioProvider extends MessageProvider {
|
|
|
164
223
|
hasMessage: Boolean(payload.message || payload.body),
|
|
165
224
|
hasMedia: Boolean(payload.fileUrl)
|
|
166
225
|
});
|
|
167
|
-
await sender(payload);
|
|
168
|
-
|
|
226
|
+
const response = await sender(payload);
|
|
227
|
+
const messageId = response?.result?.sid || null;
|
|
228
|
+
await updateStatus('sent', messageId);
|
|
169
229
|
} catch (error) {
|
|
230
|
+
await updateStatus('failed', null, error);
|
|
170
231
|
console.error(`Scheduled message failed: ${error.message}`);
|
|
171
232
|
}
|
|
172
233
|
}, delay);
|
|
@@ -253,7 +314,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
253
314
|
mediaType,
|
|
254
315
|
fileName: existingFileName || `${sanitizedBase}.${extension}`,
|
|
255
316
|
fileSize: buffer.length,
|
|
256
|
-
caption: messageData.
|
|
317
|
+
caption: messageData.body || '',
|
|
257
318
|
metadata: {
|
|
258
319
|
originalUrl: fileUrl,
|
|
259
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,25 @@ 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,
|
|
424
|
+
processed: true
|
|
419
425
|
});
|
|
420
426
|
}
|
|
421
427
|
} catch (error) {
|
|
@@ -427,7 +433,24 @@ class NexusMessaging {
|
|
|
427
433
|
if (this.messageStorage) {
|
|
428
434
|
await this.messageStorage.saveInteractive(messageData);
|
|
429
435
|
}
|
|
430
|
-
|
|
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;
|
|
431
454
|
}
|
|
432
455
|
|
|
433
456
|
async handleMedia(messageData) {
|
|
@@ -440,7 +463,7 @@ class NexusMessaging {
|
|
|
440
463
|
|
|
441
464
|
async handleMediaWithAssistant(messageData) {
|
|
442
465
|
try {
|
|
443
|
-
const { from,
|
|
466
|
+
const { from, body } = this._extractAssistantInputs(messageData);
|
|
444
467
|
|
|
445
468
|
if (!from) {
|
|
446
469
|
console.warn('Unable to resolve sender for media message, skipping automatic reply.');
|
|
@@ -456,8 +479,8 @@ class NexusMessaging {
|
|
|
456
479
|
return null;
|
|
457
480
|
})();
|
|
458
481
|
|
|
459
|
-
const fallbackMessage =
|
|
460
|
-
?
|
|
482
|
+
const fallbackMessage = body && body.trim().length > 0
|
|
483
|
+
? body
|
|
461
484
|
: `Media received (${mediaDescriptor || 'attachment'})`;
|
|
462
485
|
|
|
463
486
|
const response = await replyAssistant(from, fallbackMessage);
|
|
@@ -465,7 +488,7 @@ class NexusMessaging {
|
|
|
465
488
|
if (response) {
|
|
466
489
|
await this.sendMessage({
|
|
467
490
|
code: from,
|
|
468
|
-
|
|
491
|
+
body: response
|
|
469
492
|
});
|
|
470
493
|
}
|
|
471
494
|
} catch (error) {
|
|
@@ -507,8 +530,8 @@ class NexusMessaging {
|
|
|
507
530
|
messageData.fileUrl = messageData.fileUrl || mediaPayload.metadata?.presignedUrl || null;
|
|
508
531
|
messageData.fileType = messageData.fileType || mediaPayload.mediaType;
|
|
509
532
|
messageData.caption = messageData.caption || primary.caption;
|
|
510
|
-
if (!messageData.
|
|
511
|
-
messageData.
|
|
533
|
+
if (!messageData.body && messageData.caption) {
|
|
534
|
+
messageData.body = messageData.caption;
|
|
512
535
|
}
|
|
513
536
|
messageData.isMedia = true;
|
|
514
537
|
|
|
@@ -519,7 +542,7 @@ class NexusMessaging {
|
|
|
519
542
|
const messageObj = convertTwilioToInternalFormat(raw);
|
|
520
543
|
const values = getMessageValues(
|
|
521
544
|
messageObj,
|
|
522
|
-
messageData.
|
|
545
|
+
messageData.body || messageData.caption || '',
|
|
523
546
|
null,
|
|
524
547
|
true
|
|
525
548
|
);
|
|
@@ -604,8 +627,9 @@ class NexusMessaging {
|
|
|
604
627
|
if (botResponse && this.provider) {
|
|
605
628
|
await this.provider.sendMessage({
|
|
606
629
|
to: chatId,
|
|
607
|
-
|
|
608
|
-
type: 'text'
|
|
630
|
+
body: botResponse,
|
|
631
|
+
type: 'text',
|
|
632
|
+
processed: true
|
|
609
633
|
});
|
|
610
634
|
}
|
|
611
635
|
|
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
|
});
|
|
@@ -16,7 +16,7 @@ class MessageParser {
|
|
|
16
16
|
*/
|
|
17
17
|
parseMessage(rawMessage) {
|
|
18
18
|
const messageData = {
|
|
19
|
-
id: rawMessage.id || rawMessage.key?.id,
|
|
19
|
+
id: rawMessage.id || rawMessage.key?.id || rawMessage.MessageSid,
|
|
20
20
|
from: this.extractSender(rawMessage),
|
|
21
21
|
timestamp: rawMessage.timestamp || Date.now(),
|
|
22
22
|
raw: rawMessage
|
|
@@ -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) {
|