@peopl-health/nexus 1.5.9 → 1.5.10
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
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
|
|
2
|
+
require('dotenv').config();
|
|
3
|
+
const { Nexus, setupDefaultRoutes } = require('@peopl-health/nexus');
|
|
3
4
|
|
|
4
5
|
const app = express();
|
|
5
6
|
app.use(express.json());
|
|
6
7
|
|
|
7
8
|
async function startServer() {
|
|
8
9
|
// Initialize Nexus with all services
|
|
9
|
-
const nexus = new
|
|
10
|
+
const nexus = new Nexus();
|
|
10
11
|
|
|
11
12
|
await nexus.initialize({
|
|
12
13
|
// MongoDB connection
|
|
@@ -17,7 +18,7 @@ async function startServer() {
|
|
|
17
18
|
providerConfig: {
|
|
18
19
|
accountSid: process.env.TWILIO_ACCOUNT_SID,
|
|
19
20
|
authToken: process.env.TWILIO_AUTH_TOKEN,
|
|
20
|
-
|
|
21
|
+
whatsappNumber: process.env.TWILIO_WHATSAPP_NUMBER
|
|
21
22
|
}
|
|
22
23
|
});
|
|
23
24
|
|
|
@@ -27,7 +28,7 @@ async function startServer() {
|
|
|
27
28
|
// Add webhook endpoint for incoming messages
|
|
28
29
|
app.post('/webhook', async (req, res) => {
|
|
29
30
|
try {
|
|
30
|
-
await nexus.processIncomingMessage(req.body);
|
|
31
|
+
await nexus.messaging.processIncomingMessage(req.body);
|
|
31
32
|
res.status(200).send('OK');
|
|
32
33
|
} catch (error) {
|
|
33
34
|
console.error('Webhook error:', error);
|
|
@@ -38,10 +39,10 @@ async function startServer() {
|
|
|
38
39
|
// Custom endpoint example
|
|
39
40
|
app.get('/status', (req, res) => {
|
|
40
41
|
res.json({
|
|
41
|
-
connected: nexus.isConnected(),
|
|
42
|
+
connected: nexus.messaging.isConnected(),
|
|
42
43
|
provider: 'twilio',
|
|
43
|
-
mongodb: nexus.mongodb?.readyState === 1,
|
|
44
|
-
airtable: !!nexus.airtable
|
|
44
|
+
mongodb: nexus.messaging.mongodb?.readyState === 1,
|
|
45
|
+
airtable: !!nexus.messaging.airtable
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
48
|
|
|
@@ -44,10 +44,11 @@ class TwilioProvider extends MessageProvider {
|
|
|
44
44
|
throw new Error('Twilio provider not initialized');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const {
|
|
47
|
+
const { code, message, fileUrl, fileType, variables, contentSid } = messageData;
|
|
48
48
|
|
|
49
49
|
const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
|
|
50
|
-
const formattedTo = this.ensureWhatsAppFormat(
|
|
50
|
+
const formattedTo = this.ensureWhatsAppFormat(code);
|
|
51
|
+
|
|
51
52
|
|
|
52
53
|
if (!formattedFrom || !formattedTo) {
|
|
53
54
|
throw new Error('Invalid sender or recipient number');
|
|
@@ -60,6 +61,12 @@ class TwilioProvider extends MessageProvider {
|
|
|
60
61
|
|
|
61
62
|
// Handle template messages
|
|
62
63
|
if (contentSid) {
|
|
64
|
+
// Render template and add to messageData for storage
|
|
65
|
+
const renderedMessage = await this.renderTemplate(contentSid, variables);
|
|
66
|
+
if (renderedMessage) {
|
|
67
|
+
messageData.body = renderedMessage; // Add rendered content for storage
|
|
68
|
+
}
|
|
69
|
+
|
|
63
70
|
messageParams.contentSid = contentSid;
|
|
64
71
|
if (variables && Object.keys(variables).length > 0) {
|
|
65
72
|
const formattedVariables = {};
|
|
@@ -140,17 +147,18 @@ class TwilioProvider extends MessageProvider {
|
|
|
140
147
|
: async (payload) => await this.sendMessage(payload);
|
|
141
148
|
|
|
142
149
|
console.log('[TwilioProvider] Scheduled message created', {
|
|
143
|
-
to: scheduledMessage.
|
|
150
|
+
to: scheduledMessage.code,
|
|
144
151
|
delay,
|
|
145
152
|
hasContentSid: Boolean(scheduledMessage.contentSid)
|
|
146
153
|
});
|
|
147
154
|
|
|
148
155
|
setTimeout(async () => {
|
|
149
156
|
try {
|
|
150
|
-
|
|
157
|
+
// Convert Mongoose document to plain object if needed
|
|
158
|
+
const payload = scheduledMessage.toObject ? scheduledMessage.toObject() : { ...scheduledMessage };
|
|
151
159
|
delete payload.__nexusSend;
|
|
152
160
|
console.log('[TwilioProvider] Timer fired', {
|
|
153
|
-
to: payload.
|
|
161
|
+
to: payload.code,
|
|
154
162
|
hasMessage: Boolean(payload.message || payload.body),
|
|
155
163
|
hasMedia: Boolean(payload.fileUrl)
|
|
156
164
|
});
|
|
@@ -311,6 +319,170 @@ class TwilioProvider extends MessageProvider {
|
|
|
311
319
|
}
|
|
312
320
|
}
|
|
313
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Render template content with variables
|
|
324
|
+
* @param {string} contentSid - The Twilio content SID
|
|
325
|
+
* @param {Object} variables - The variables object with keys like "1", "2"
|
|
326
|
+
* @returns {Promise<string|null>} The rendered message content or null if not found
|
|
327
|
+
*/
|
|
328
|
+
async renderTemplate(contentSid, variables) {
|
|
329
|
+
try {
|
|
330
|
+
if (!contentSid) return null;
|
|
331
|
+
|
|
332
|
+
const template = await this.getTemplate(contentSid);
|
|
333
|
+
|
|
334
|
+
if (!template || !template.types) {
|
|
335
|
+
console.warn('[TwilioProvider] Template not found or has no types:', contentSid);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Extract text content from different template types
|
|
340
|
+
let textContent = this.extractTextFromTemplate(template);
|
|
341
|
+
|
|
342
|
+
if (!textContent) {
|
|
343
|
+
console.warn('[TwilioProvider] No text content found in template:', contentSid);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Render variables if provided
|
|
348
|
+
if (variables && typeof variables === 'object' && Object.keys(variables).length > 0) {
|
|
349
|
+
return this.renderTemplateWithVariables(textContent, variables);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return textContent.trim();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error('[TwilioProvider] Error rendering template:', error.message);
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Extract text content from different template types
|
|
361
|
+
* @param {Object} template - The Twilio template object
|
|
362
|
+
* @returns {string} The extracted text content
|
|
363
|
+
*/
|
|
364
|
+
extractTextFromTemplate(template) {
|
|
365
|
+
const types = template.types || {};
|
|
366
|
+
|
|
367
|
+
// Handle plain text templates
|
|
368
|
+
if (types['twilio/text']) {
|
|
369
|
+
return types['twilio/text'].body || '';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Handle quick reply templates
|
|
373
|
+
if (types['twilio/quick-reply']) {
|
|
374
|
+
const quickReply = types['twilio/quick-reply'];
|
|
375
|
+
let text = quickReply.body || '';
|
|
376
|
+
|
|
377
|
+
// Add quick reply options
|
|
378
|
+
if (quickReply.actions && Array.isArray(quickReply.actions)) {
|
|
379
|
+
const options = quickReply.actions
|
|
380
|
+
.filter(action => action.title)
|
|
381
|
+
.map(action => `• ${action.title}`)
|
|
382
|
+
.join('\n');
|
|
383
|
+
if (options) {
|
|
384
|
+
text += (text ? '\n\n' : '') + options;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return text;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Handle list templates
|
|
392
|
+
if (types['twilio/list']) {
|
|
393
|
+
const list = types['twilio/list'];
|
|
394
|
+
let text = list.body || '';
|
|
395
|
+
|
|
396
|
+
// Add list items
|
|
397
|
+
if (list.items && Array.isArray(list.items)) {
|
|
398
|
+
const items = list.items
|
|
399
|
+
.filter(item => item.title)
|
|
400
|
+
.map((item, index) => `${index + 1}. ${item.title}`)
|
|
401
|
+
.join('\n');
|
|
402
|
+
if (items) {
|
|
403
|
+
text += (text ? '\n\n' : '') + items;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return text;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Handle button templates
|
|
411
|
+
if (types['twilio/button']) {
|
|
412
|
+
const button = types['twilio/button'];
|
|
413
|
+
let text = button.body || '';
|
|
414
|
+
|
|
415
|
+
// Add button options
|
|
416
|
+
if (button.actions && Array.isArray(button.actions)) {
|
|
417
|
+
const buttons = button.actions
|
|
418
|
+
.filter(action => action.title)
|
|
419
|
+
.map(action => `[${action.title}]`)
|
|
420
|
+
.join(' ');
|
|
421
|
+
if (buttons) {
|
|
422
|
+
text += (text ? '\n\n' : '') + buttons;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return text;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Handle flow templates (fallback to body)
|
|
430
|
+
if (types['twilio/flows']) {
|
|
431
|
+
return types['twilio/flows'].body || '';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Handle media templates (extract caption)
|
|
435
|
+
if (types['twilio/media']) {
|
|
436
|
+
return types['twilio/media'].caption || '';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Fallback: try to find any body content
|
|
440
|
+
for (const typeKey of Object.keys(types)) {
|
|
441
|
+
const type = types[typeKey];
|
|
442
|
+
if (type && typeof type === 'object' && type.body) {
|
|
443
|
+
return type.body;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return '';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Render template content with variables
|
|
452
|
+
* @param {string} templateBody - The template body with placeholders like {{1}}, {{2}}
|
|
453
|
+
* @param {Object} variables - The variables object with keys like "1", "2"
|
|
454
|
+
* @returns {string} The rendered message content
|
|
455
|
+
*/
|
|
456
|
+
renderTemplateWithVariables(templateBody, variables) {
|
|
457
|
+
if (!templateBody || typeof templateBody !== 'string') {
|
|
458
|
+
return '';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!variables || typeof variables !== 'object') {
|
|
462
|
+
return templateBody;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
let rendered = templateBody;
|
|
467
|
+
|
|
468
|
+
// Replace placeholders like {{1}}, {{2}}, etc. with variable values
|
|
469
|
+
Object.keys(variables).forEach(key => {
|
|
470
|
+
const placeholder = `{{${key}}}`;
|
|
471
|
+
const value = variables[key] || '';
|
|
472
|
+
|
|
473
|
+
// Simple string replacement - more reliable than regex for this use case
|
|
474
|
+
if (rendered.includes(placeholder)) {
|
|
475
|
+
rendered = rendered.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return rendered.trim();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.warn('[TwilioProvider] Error rendering template variables:', error.message);
|
|
482
|
+
return templateBody; // Return original template if rendering fails
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
314
486
|
/**
|
|
315
487
|
* Check template approval status using Twilio Content API helpers
|
|
316
488
|
*/
|
|
@@ -68,7 +68,8 @@ async function insertMessage(values) {
|
|
|
68
68
|
reply_id: values.reply_id,
|
|
69
69
|
from_me: values.from_me,
|
|
70
70
|
processed: skipNumbers.includes(values.numero),
|
|
71
|
-
media: values.media ? values.media : null
|
|
71
|
+
media: values.media ? values.media : null,
|
|
72
|
+
content_sid: values.content_sid || null
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
await Message.findOneAndUpdate(
|
|
@@ -66,10 +66,11 @@ class MongoStorage {
|
|
|
66
66
|
from: messageData?.from,
|
|
67
67
|
provider: messageData?.provider || 'unknown',
|
|
68
68
|
hasRaw: Boolean(messageData?.raw),
|
|
69
|
-
hasMedia: Boolean(messageData?.media || messageData?.fileUrl)
|
|
69
|
+
hasMedia: Boolean(messageData?.media || messageData?.fileUrl),
|
|
70
|
+
hasContentSid: Boolean(messageData?.contentSid)
|
|
70
71
|
});
|
|
71
72
|
const enrichedMessage = await this._enrichTwilioMedia(messageData);
|
|
72
|
-
const values = this.
|
|
73
|
+
const values = this.buildMessageValues(enrichedMessage);
|
|
73
74
|
const { insertMessage } = require('../models/messageModel');
|
|
74
75
|
await insertMessage(values);
|
|
75
76
|
console.log('[MongoStorage] Message stored', {
|
|
@@ -166,7 +167,8 @@ class MongoStorage {
|
|
|
166
167
|
return trimmed;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
|
|
171
|
+
buildMessageValues(messageData = {}) {
|
|
170
172
|
const numero = messageData.to || messageData.code || messageData.numero || messageData.from;
|
|
171
173
|
const rawNumero = typeof numero === 'string' ? numero : '';
|
|
172
174
|
const normalizedNumero = this.normalizeNumero(rawNumero);
|
|
@@ -175,7 +177,16 @@ class MongoStorage {
|
|
|
175
177
|
const now = new Date();
|
|
176
178
|
const timestamp = now.toISOString();
|
|
177
179
|
const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName || runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
|
|
178
|
-
|
|
180
|
+
|
|
181
|
+
// Use message body directly (template rendering is now handled by the provider)
|
|
182
|
+
let textBody = messageData.message || messageData.body;
|
|
183
|
+
|
|
184
|
+
if (!textBody && isMedia) {
|
|
185
|
+
textBody = `[Media:${messageData.fileType || 'attachment'}]`;
|
|
186
|
+
} else if (!textBody) {
|
|
187
|
+
textBody = '';
|
|
188
|
+
}
|
|
189
|
+
|
|
179
190
|
const providerId = messageData.messageId || messageData.sid || messageData.id || messageData._id || `pending-${now.getTime()}-${Math.floor(Math.random()*1000)}`;
|
|
180
191
|
|
|
181
192
|
const media = messageData.media || (messageData.fileUrl ? {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peopl-health/nexus",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.10",
|
|
4
4
|
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whatsapp",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"airtable": "^0.12.2",
|
|
74
74
|
"aws-sdk": "2.1674.0",
|
|
75
75
|
"axios": "^1.5.0",
|
|
76
|
-
"dotenv": "^16.
|
|
76
|
+
"dotenv": "^16.6.1",
|
|
77
77
|
"moment-timezone": "^0.5.43",
|
|
78
78
|
"mongoose": "^7.5.0",
|
|
79
79
|
"multer": "1.4.5-lts.1",
|
|
@@ -102,4 +102,4 @@
|
|
|
102
102
|
"publishConfig": {
|
|
103
103
|
"access": "public"
|
|
104
104
|
}
|
|
105
|
-
}
|
|
105
|
+
}
|