@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.
@@ -1,12 +1,13 @@
1
1
  const express = require('express');
2
- const { NexusMessaging, setupDefaultRoutes } = require('@peopl-health/nexus');
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 NexusMessaging();
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
- phoneNumber: process.env.TWILIO_PHONE_NUMBER
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 { to, message, fileUrl, fileType, variables, contentSid } = messageData;
47
+ const { code, message, fileUrl, fileType, variables, contentSid } = messageData;
48
48
 
49
49
  const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
50
- const formattedTo = this.ensureWhatsAppFormat(to);
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.to || scheduledMessage.code,
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
- const payload = { ...scheduledMessage };
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.to || payload.code,
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.buildLegacyMessageValues(enrichedMessage);
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
- buildLegacyMessageValues(messageData = {}) {
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
- const textBody = messageData.message || messageData.body || (messageData.contentSid ? `[Template:${messageData.contentSid}]` : isMedia ? `[Media:${messageData.fileType || 'attachment'}]` : '');
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.9",
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.4.7",
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
+ }