@peopl-health/nexus 2.5.9 → 2.5.11
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.
|
@@ -105,15 +105,52 @@ class TwilioProvider extends MessageProvider {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// Validate message has content
|
|
109
108
|
if (!messageParams.body && !messageParams.mediaUrl && !messageParams.contentSid) {
|
|
110
109
|
throw new Error('Message must have body, media URL, or content SID');
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
let result;
|
|
113
|
+
const chunks = messageParams.body && messageParams.body.length > 1600 && !messageParams.mediaUrl && !messageParams.contentSid
|
|
114
|
+
? this.splitMessageAtWordBoundaries(messageParams.body)
|
|
115
|
+
: null;
|
|
116
|
+
|
|
117
|
+
if (chunks) {
|
|
118
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
119
|
+
const chunkParams = { ...messageParams, body: chunks[i] };
|
|
120
|
+
result = await this.twilioClient.messages.create(chunkParams);
|
|
121
|
+
if (this.messageStorage && typeof this.messageStorage.saveMessage === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
await this.messageStorage.saveMessage({
|
|
124
|
+
...messageData,
|
|
125
|
+
body: chunks[i],
|
|
126
|
+
code: formattedCode,
|
|
127
|
+
from: formattedFrom,
|
|
128
|
+
messageId: result.sid,
|
|
129
|
+
provider: 'twilio',
|
|
130
|
+
timestamp: new Date(),
|
|
131
|
+
fromMe: true,
|
|
132
|
+
processed: messageData.processed !== undefined ? messageData.processed : false,
|
|
133
|
+
statusInfo: {
|
|
134
|
+
status: result.status ? result.status.toLowerCase() : null,
|
|
135
|
+
updatedAt: result.dateCreated || new Date()
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
logger.info('[TwilioProvider] Message chunk persisted', { messageId: result.sid, chunk: i + 1, total: chunks.length });
|
|
139
|
+
} catch (storageError) {
|
|
140
|
+
logger.error('TwilioProvider storage failed:', storageError);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (i < chunks.length - 1) {
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
logger.info('[TwilioProvider] Sending message', messageParams);
|
|
149
|
+
try {
|
|
150
|
+
result = await this.twilioClient.messages.create(messageParams);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new Error(`Twilio send failed: ${error.message}`);
|
|
153
|
+
}
|
|
117
154
|
if (this.messageStorage && typeof this.messageStorage.saveMessage === 'function') {
|
|
118
155
|
try {
|
|
119
156
|
await this.messageStorage.saveMessage({
|
|
@@ -135,17 +172,15 @@ class TwilioProvider extends MessageProvider {
|
|
|
135
172
|
logger.error('TwilioProvider storage failed:', storageError);
|
|
136
173
|
}
|
|
137
174
|
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
success: true,
|
|
141
|
-
messageId: result.sid,
|
|
142
|
-
provider: 'twilio',
|
|
143
|
-
status: result.status,
|
|
144
|
-
result
|
|
145
|
-
};
|
|
146
|
-
} catch (error) {
|
|
147
|
-
throw new Error(`Twilio send failed: ${error.message}`);
|
|
148
175
|
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
messageId: result.sid,
|
|
180
|
+
provider: 'twilio',
|
|
181
|
+
status: result.status,
|
|
182
|
+
result
|
|
183
|
+
};
|
|
149
184
|
}
|
|
150
185
|
|
|
151
186
|
async sendTypingIndicator(messageId) {
|
|
@@ -394,6 +429,46 @@ class TwilioProvider extends MessageProvider {
|
|
|
394
429
|
return Math.max(0, targetTime.getTime() - now.getTime());
|
|
395
430
|
}
|
|
396
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Split a message into chunks at sentence boundaries, respecting Twilio's character limit
|
|
434
|
+
* @param {string} text - The message text to split
|
|
435
|
+
* @param {number} maxLength - Maximum length per chunk (default: 1600)
|
|
436
|
+
* @returns {Array<string>} Array of message chunks
|
|
437
|
+
*/
|
|
438
|
+
splitMessageAtWordBoundaries(text, maxLength = 1600) {
|
|
439
|
+
if (!text || text.length <= maxLength) return [text];
|
|
440
|
+
const chunks = [];
|
|
441
|
+
let remaining = text;
|
|
442
|
+
while (remaining.length > maxLength) {
|
|
443
|
+
let splitIndex = -1;
|
|
444
|
+
const searchStart = Math.max(0, maxLength - 500); // Look back up to 500 chars
|
|
445
|
+
const searchArea = remaining.substring(searchStart, maxLength + 1);
|
|
446
|
+
|
|
447
|
+
const regex = /[.!?]\s/g;
|
|
448
|
+
let match;
|
|
449
|
+
let lastMatchIndex = -1;
|
|
450
|
+
|
|
451
|
+
while ((match = regex.exec(searchArea)) !== null) {
|
|
452
|
+
const absoluteIndex = searchStart + match.index;
|
|
453
|
+
if (absoluteIndex <= maxLength) {
|
|
454
|
+
lastMatchIndex = absoluteIndex + match[0].length;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (lastMatchIndex > 0) {
|
|
459
|
+
splitIndex = lastMatchIndex;
|
|
460
|
+
} else {
|
|
461
|
+
splitIndex = remaining.lastIndexOf(' ', maxLength);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const chunk = remaining.substring(0, splitIndex === -1 ? maxLength : splitIndex).trim();
|
|
465
|
+
if (chunk) chunks.push(chunk);
|
|
466
|
+
remaining = remaining.substring(splitIndex === -1 ? maxLength : splitIndex).trim();
|
|
467
|
+
}
|
|
468
|
+
if (remaining) chunks.push(remaining);
|
|
469
|
+
return chunks;
|
|
470
|
+
}
|
|
471
|
+
|
|
397
472
|
/**
|
|
398
473
|
* List templates from Twilio Content API
|
|
399
474
|
* @param {Object} options - Query options
|
|
@@ -14,6 +14,7 @@ const Monitoreo_ID = require('./runtimeConfig').get('AIRTABLE_MONITOREO_ID') ||
|
|
|
14
14
|
const Programa_Juntas_ID = require('./runtimeConfig').get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
|
|
15
15
|
const Symptoms_ID = require('./runtimeConfig').get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
|
|
16
16
|
const Webinars_Leads_ID = require('./runtimeConfig').get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
|
|
17
|
+
const Product_ID = require('./runtimeConfig').get('AIRTABLE_PRODUCT_ID') || 'appu2YDW2pKDYLL5H';
|
|
17
18
|
|
|
18
19
|
// Initialize Airtable only if API key is provided
|
|
19
20
|
let airtable = null;
|
|
@@ -29,7 +30,8 @@ const BASE_MAP = {
|
|
|
29
30
|
monitoreo: Monitoreo_ID,
|
|
30
31
|
programa: Programa_Juntas_ID,
|
|
31
32
|
symptoms: Symptoms_ID,
|
|
32
|
-
webinars: Webinars_Leads_ID
|
|
33
|
+
webinars: Webinars_Leads_ID,
|
|
34
|
+
product: Product_ID
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
module.exports = {
|
|
@@ -43,7 +45,7 @@ module.exports = {
|
|
|
43
45
|
Programa_Juntas_ID,
|
|
44
46
|
Symptoms_ID,
|
|
45
47
|
Webinars_Leads_ID,
|
|
46
|
-
|
|
48
|
+
Product_ID,
|
|
47
49
|
// Helper function to get base by ID
|
|
48
50
|
getBase: (baseKeyOrId = require('./runtimeConfig').get('AIRTABLE_BASE_ID')) => {
|
|
49
51
|
if (!airtable) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { addRecord, getRecordByFilter } = require('../services/airtableService');
|
|
2
|
+
const { Product_ID } = require('../config/airtableConfig');
|
|
3
|
+
const { logger } = require('../utils/logger');
|
|
4
|
+
|
|
5
|
+
async function logCaseDocumentationToAirtable(reporter, whatsapp_id, tableName, dynamicFields = {}) {
|
|
6
|
+
try {
|
|
7
|
+
let patientId = null;
|
|
8
|
+
try {
|
|
9
|
+
const patientRecords = await getRecordByFilter(Product_ID, 'estado_general', `{whatsapp_id}='${whatsapp_id}'`);
|
|
10
|
+
if (patientRecords && patientRecords.length > 0) {
|
|
11
|
+
patientId = patientRecords[0].product_record_id;
|
|
12
|
+
}
|
|
13
|
+
} catch (err) {
|
|
14
|
+
logger.warn('Could not find patient in estado_general:', err.message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const airtableData = {
|
|
18
|
+
reporter,
|
|
19
|
+
...(patientId && { patient_id: [patientId] }),
|
|
20
|
+
...dynamicFields
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
Object.keys(airtableData).forEach(key => {
|
|
24
|
+
if (airtableData[key] === undefined) {
|
|
25
|
+
delete airtableData[key];
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await addRecord(Product_ID, tableName, airtableData);
|
|
30
|
+
logger.info(`Case documentation logged to Airtable successfully in table: ${tableName}`);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error('Error logging case documentation to Airtable:', error);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const caseDocumentationController = async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { reporter, whatsapp_id, table_name, ...dynamicFields } = req.body;
|
|
40
|
+
|
|
41
|
+
if (!reporter) { return res.status(400).json({ success: false, error: 'Reporter username is required' }); }
|
|
42
|
+
if (!whatsapp_id) { return res.status(400).json({ success: false, error: 'WhatsApp ID is required' }); }
|
|
43
|
+
if (!table_name) { return res.status(400).json({ success: false, error: 'Table name is required' }); }
|
|
44
|
+
|
|
45
|
+
logCaseDocumentationToAirtable(reporter, whatsapp_id, table_name, dynamicFields).catch(err =>
|
|
46
|
+
logger.error('Background case documentation logging failed:', err)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
res.status(201).json({ success: true, message: 'Case documentation submitted successfully' });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error('Error submitting case documentation:', error);
|
|
52
|
+
res.status(500).json({ success: false, error: error.message });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
module.exports = { caseDocumentationController };
|
package/lib/routes/index.js
CHANGED
|
@@ -23,7 +23,8 @@ const conversationRouteDefinitions = {
|
|
|
23
23
|
'POST /reply': 'getConversationReplyController',
|
|
24
24
|
'POST /send-template': 'sendTemplateToNewNumberController',
|
|
25
25
|
'POST /:phoneNumber/read': 'markMessagesAsReadController',
|
|
26
|
-
'POST /report-bug': 'reportBugController'
|
|
26
|
+
'POST /report-bug': 'reportBugController',
|
|
27
|
+
'POST /case-documentation': 'caseDocumentationController'
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const mediaRouteDefinitions = {
|
|
@@ -91,6 +92,7 @@ const createRouter = (routeDefinitions, controllers) => {
|
|
|
91
92
|
// Import built-in controllers
|
|
92
93
|
const assistantController = require('../controllers/assistantController');
|
|
93
94
|
const bugReportController = require('../controllers/bugReportController');
|
|
95
|
+
const caseDocumentationController = require('../controllers/caseDocumentationController');
|
|
94
96
|
const conversationController = require('../controllers/conversationController');
|
|
95
97
|
const interactionController = require('../controllers/interactionController');
|
|
96
98
|
const mediaController = require('../controllers/mediaController');
|
|
@@ -125,6 +127,7 @@ const builtInControllers = {
|
|
|
125
127
|
sendTemplateToNewNumberController: conversationController.sendTemplateToNewNumberController,
|
|
126
128
|
markMessagesAsReadController: conversationController.markMessagesAsReadController,
|
|
127
129
|
reportBugController: bugReportController.reportBugController,
|
|
130
|
+
caseDocumentationController: caseDocumentationController.caseDocumentationController,
|
|
128
131
|
|
|
129
132
|
// Interaction controllers
|
|
130
133
|
addInteractionController: interactionController.addInteractionController,
|