@peopl-health/nexus 1.1.1 → 1.1.3

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/README.md CHANGED
@@ -231,52 +231,8 @@ const { setupDefaultRoutes } = require('@peopl-health/nexus');
231
231
 
232
232
  const app = express();
233
233
 
234
- // Define your controllers
235
- const myControllers = {
236
- // Assistant controllers
237
- activeAssistantController: (req, res) => { /* your logic */ },
238
- createAssistantController: (req, res) => { /* your logic */ },
239
- listAssistantController: (req, res) => { /* your logic */ },
240
- addInsAssistantController: (req, res) => { /* your logic */ },
241
- addMsgAssistantController: (req, res) => { /* your logic */ },
242
- getInfoAssistantController: (req, res) => { /* your logic */ },
243
- switchAssistantController: (req, res) => { /* your logic */ },
244
- stopAssistantController: (req, res) => { /* your logic */ },
245
-
246
- // Conversation controllers
247
- getConversationController: (req, res) => { /* your logic */ },
248
- searchConversationsController: (req, res) => { /* your logic */ },
249
- getConversationsByNameController: (req, res) => { /* your logic */ },
250
- getConversationMessagesController: (req, res) => { /* your logic */ },
251
- getNewMessagesController: (req, res) => { /* your logic */ },
252
- getConversationReplyController: (req, res) => { /* your logic */ },
253
- sendTemplateToNewNumberController: (req, res) => { /* your logic */ },
254
- markMessagesAsReadController: (req, res) => { /* your logic */ },
255
-
256
- // Media controllers
257
- getMediaController: (req, res) => { /* your logic */ },
258
- handleFileUpload: (req, res) => { /* your logic */ },
259
-
260
- // Message controllers
261
- sendMessageController: (req, res) => { /* your logic */ },
262
- sendBulkMessageController: (req, res) => { /* your logic */ },
263
- sendBulkMessageAirtableController: (req, res) => { /* your logic */ },
264
-
265
- // Template controllers
266
- createTemplate: (req, res) => { /* your logic */ },
267
- listTemplates: (req, res) => { /* your logic */ },
268
- getPredefinedTemplates: (req, res) => { /* your logic */ },
269
- getTemplate: (req, res) => { /* your logic */ },
270
- getCompleteTemplate: (req, res) => { /* your logic */ },
271
- createFlow: (req, res) => { /* your logic */ },
272
- deleteFlow: (req, res) => { /* your logic */ },
273
- submitForApproval: (req, res) => { /* your logic */ },
274
- checkApprovalStatus: (req, res) => { /* your logic */ },
275
- deleteTemplate: (req, res) => { /* your logic */ }
276
- };
277
-
278
- // Setup all default routes with one line
279
- setupDefaultRoutes(app, myControllers);
234
+ // Setup all default routes with built-in controllers (one line!)
235
+ setupDefaultRoutes(app);
280
236
 
281
237
  // Add your custom routes
282
238
  app.get('/api/custom', myCustomController);
@@ -1,6 +1,10 @@
1
- const { Nexus } = require('@peopl/nexus');
1
+ const { Nexus, setupDefaultRoutes } = require('@peopl/nexus');
2
+ const express = require('express');
2
3
  require('dotenv').config();
3
4
 
5
+ const app = express();
6
+ app.use(express.json());
7
+
4
8
  async function main() {
5
9
  const nexus = new Nexus();
6
10
 
@@ -14,6 +18,9 @@ async function main() {
14
18
  }
15
19
  });
16
20
 
21
+ // Setup all default routes (no controllers needed!)
22
+ setupDefaultRoutes(app);
23
+
17
24
  // Set up handlers with minimal code
18
25
  nexus.setHandlers({
19
26
  onMessage: async (messageData) => {
@@ -37,7 +44,16 @@ async function main() {
37
44
  }
38
45
  });
39
46
 
40
- console.log('Nexus started - send messages to test');
47
+ // Webhook endpoint
48
+ app.post('/webhook', async (req, res) => {
49
+ await nexus.processMessage(req.body);
50
+ res.sendStatus(200);
51
+ });
52
+
53
+ const PORT = process.env.PORT || 3000;
54
+ app.listen(PORT, () => {
55
+ console.log(`Nexus server running on port ${PORT}`);
56
+ });
41
57
 
42
58
  } catch (error) {
43
59
  console.error('Error:', error);
@@ -0,0 +1,45 @@
1
+ const Airtable = require('airtable');
2
+
3
+ const airtableConfig = {
4
+ apiKey: process.env.AIRTABLE_API_KEY,
5
+ };
6
+
7
+ // Configurable base IDs - users can override via environment variables
8
+ const Calendar_ID = process.env.AIRTABLE_CALENDAR_ID || 'appIjEstWR6972tbF';
9
+ const Config_ID = process.env.AIRTABLE_CONFIG_ID || 'app9K4EvGI8McC8jF';
10
+ const Historial_Clinico_ID = process.env.AIRTABLE_HISTORIAL_CLINICO_ID || 'appdUpGUS06XIzVnY';
11
+ const Logging_ID = process.env.AIRTABLE_LOGGING_ID || 'appQ7YhzfebRDbSPJ';
12
+ const Monitoreo_ID = process.env.AIRTABLE_MONITOREO_ID || 'appdvraKSdp0XVn5n';
13
+ const Programa_Juntas_ID = process.env.AIRTABLE_PROGRAMA_JUNTAS_ID || 'appKFWzkcDEWlrXBE';
14
+ const Symptoms_ID = process.env.AIRTABLE_SYMPTOMS_ID || 'appQRhZlQ9tMfYZWJ';
15
+ const Webinars_Leads_ID = process.env.AIRTABLE_WEBINARS_LEADS_ID || 'appzjpVXTI0TgqGPq';
16
+
17
+ // Initialize Airtable only if API key is provided
18
+ let airtable = null;
19
+ if (airtableConfig.apiKey) {
20
+ airtable = new Airtable({ apiKey: airtableConfig.apiKey });
21
+ }
22
+
23
+ module.exports = {
24
+ airtable,
25
+ config: airtableConfig,
26
+ Calendar_ID,
27
+ Config_ID,
28
+ Historial_Clinico_ID,
29
+ Logging_ID,
30
+ Monitoreo_ID,
31
+ Programa_Juntas_ID,
32
+ Symptoms_ID,
33
+ Webinars_Leads_ID,
34
+
35
+ // Helper function to get base by ID
36
+ getBase: (baseId = process.env.AIRTABLE_BASE_ID) => {
37
+ if (!airtable) {
38
+ throw new Error('Airtable not configured. Please set AIRTABLE_API_KEY environment variable.');
39
+ }
40
+ if (!baseId) {
41
+ throw new Error('Airtable base ID not provided. Please set AIRTABLE_BASE_ID environment variable or pass baseId parameter.');
42
+ }
43
+ return airtable.base(baseId);
44
+ }
45
+ };
@@ -1,11 +1,6 @@
1
1
  const { MongoClient } = require('mongodb');
2
2
 
3
- let baileys;
4
- try {
5
- baileys = require('baileys');
6
- } catch (error) {
7
- baileys = null;
8
- }
3
+ const { proto, Curve, signedKeyPair, generateRegistrationId } = require('baileys');
9
4
  const { randomBytes } = require('crypto');
10
5
 
11
6
  async function connectToMongo(mongoClient) {
@@ -19,11 +14,6 @@ async function connectToMongo(mongoClient) {
19
14
  }
20
15
 
21
16
  async function initAuthCreds() {
22
- if (!baileys) {
23
- throw new Error('Baileys is required for auth credentials but not installed');
24
- }
25
-
26
- const { proto, Curve, signedKeyPair, generateRegistrationId } = baileys;
27
17
  const identityKey = Curve.generateKeyPair();
28
18
  return {
29
19
  noiseKey: Curve.generateKeyPair(),
@@ -120,8 +110,8 @@ async function useMongoDBAuthState(uri, dbName, sessionId) {
120
110
  const data = {};
121
111
  await Promise.all(ids.map(async (id) => {
122
112
  let value = await readData(`${type}-${id}`);
123
- if (type === 'app-state-sync-key' && baileys) {
124
- value = baileys.proto.Message.AppStateSyncKeyData.fromObject(value);
113
+ if (type === 'app-state-sync-key') {
114
+ value = proto.Message.AppStateSyncKeyData.fromObject(data);
125
115
  }
126
116
  data[id] = value;
127
117
  }));
@@ -17,28 +17,14 @@ try {
17
17
  // Models not available
18
18
  }
19
19
 
20
- // Optional service imports
21
- let getRecordByFilter, createAssistant, addMsgAssistant, addInsAssistant, getThreadInfo, switchAssistant, sendMessage;
22
- try {
23
- getRecordByFilter = require('../services/airtableService')?.getRecordByFilter;
24
- } catch (e) {
25
- // Service not available
26
- }
27
- try {
28
- const assistantService = require('../services/assistantService');
29
- createAssistant = assistantService.createAssistant;
30
- addMsgAssistant = assistantService.addMsgAssistant;
31
- addInsAssistant = assistantService.addInsAssistant;
32
- getThreadInfo = assistantService.getThreadInfo;
33
- switchAssistant = assistantService.switchAssistant;
34
- } catch (e) {
35
- // Service not available
36
- }
37
- try {
38
- sendMessage = require('../services/whatsappService')?.sendMessage;
39
- } catch (e) {
40
- // Service not available
41
- }
20
+ // Optional service imports - stub functions for missing services
21
+ const getRecordByFilter = () => Promise.resolve(null);
22
+ const createAssistant = () => Promise.resolve({ success: false, error: 'Service not available' });
23
+ const addMsgAssistant = () => Promise.resolve({ success: false, error: 'Service not available' });
24
+ const addInsAssistant = () => Promise.resolve({ success: false, error: 'Service not available' });
25
+ const getThreadInfo = () => Promise.resolve(null);
26
+ const switchAssistant = () => Promise.resolve({ success: false, error: 'Service not available' });
27
+ const sendMessage = () => Promise.resolve({ success: false, error: 'Service not available' });
42
28
 
43
29
 
44
30
  const activeAssistantController = async (req, res) => {
@@ -1,6 +1,6 @@
1
1
  const { Message } = require('../models/messageModel');
2
- const { sendMessage } = require('../services/whatsappService');
3
2
  const { fetchConversationData, processConversations } = require('../services/conversationService');
3
+ const { sendMessage } = require('../core/NexusMessaging');
4
4
 
5
5
  const getConversationController = async (req, res) => {
6
6
  const startTime = Date.now();
@@ -1,7 +1,17 @@
1
- const { ScheduledMessage } = require('../models/agendaMessageModel.js');
1
+ // Import from Nexus core
2
+ const { sendMessage } = require('../core/NexusMessaging');
3
+
4
+ // Stub for missing model
5
+ const ScheduledMessage = {
6
+ create: () => Promise.resolve({ success: false, error: 'Model not available' }),
7
+ find: () => Promise.resolve([]),
8
+ findById: () => Promise.resolve(null),
9
+ deleteOne: () => Promise.resolve({ success: false, error: 'Model not available' })
10
+ };
2
11
 
3
- const { getRecordByFilter } = require('../services/airtableService.js');
4
- const { sendScheduledMessage } = require('../services/whatsappService.js');
12
+ // Stub functions for missing services
13
+ const getRecordByFilter = () => Promise.resolve(null);
14
+ const sendScheduledMessage = () => Promise.resolve({ success: false, error: 'Service not available' });
5
15
 
6
16
  const moment = require('moment-timezone');
7
17
 
@@ -21,7 +31,7 @@ const sendMessageController = async (req, res) => {
21
31
  const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 2500 : new Date();
22
32
 
23
33
  try {
24
- const scheduledMessage = new ScheduledMessage({
34
+ const messageData = {
25
35
  fileUrl,
26
36
  message,
27
37
  fileType,
@@ -33,14 +43,16 @@ const sendMessageController = async (req, res) => {
33
43
  author,
34
44
  extraDelay: 0,
35
45
  variables
36
- });
37
- await scheduledMessage.save();
38
- console.log(scheduledMessage);
39
- const sentMessage = await sendScheduledMessage(scheduledMessage);
40
- res.status(200).json({
41
- status: 200,
42
- response: 'Message scheduled to be sent once!',
43
- messageId: sentMessage ? sentMessage.sid : null
46
+ };
47
+ await ScheduledMessage.create(messageData);
48
+ console.log('Sending message with data:', messageData);
49
+
50
+ const result = await sendMessage(messageData);
51
+
52
+ res.status(200).json({
53
+ success: true,
54
+ message: 'Message sent successfully',
55
+ messageId: result?.sid || result?.id
44
56
  });
45
57
  } catch (err) {
46
58
  console.error('Error scheduling individual message:', err.message);
@@ -1,11 +1,23 @@
1
- const TwilioService = require('../services/twilioService');
2
- const { handleApiError } = require('../utils/errorHandler');
1
+ // Stub functions for missing services
2
+ const TwilioService = {
3
+ createTemplate: () => Promise.resolve({ success: false, error: 'Service not available' }),
4
+ deleteTemplate: () => Promise.resolve({ success: false, error: 'Service not available' }),
5
+ listTemplates: () => Promise.resolve([]),
6
+ getTemplate: () => Promise.resolve(null)
7
+ };
8
+ const handleApiError = (res, error, message) => {
9
+ console.error(message, error);
10
+ return res.status(500).json({ success: false, error: message });
11
+ };
3
12
 
4
- // Import flow functions from templateFlowController
5
- const { createFlow, deleteFlow } = require('./templateFlowController');
6
- const { Template } = require('../templates/templateStructure');
7
- const TemplateModel = require('../models/templateModel');
8
- const predefinedTemplates = require('../templates/predefinedTemplates');
13
+ // Stub imports for missing dependencies
14
+ const Template = class { constructor() {} };
15
+ const TemplateModel = {
16
+ create: () => Promise.resolve({ success: false, error: 'Model not available' }),
17
+ find: () => Promise.resolve([]),
18
+ findById: () => Promise.resolve(null)
19
+ };
20
+ const predefinedTemplates = [];
9
21
 
10
22
  /**
11
23
  * Create a new template and store it in both Twilio and our database
@@ -616,6 +628,15 @@ const getCompleteTemplate = async (req, res) => {
616
628
  }
617
629
  };
618
630
 
631
+ // Flow functions (inline since templateFlowController was removed)
632
+ const createFlow = async (req, res) => {
633
+ return res.status(501).json({ success: false, error: 'Flow creation not available' });
634
+ };
635
+
636
+ const deleteFlow = async (req, res) => {
637
+ return res.status(501).json({ success: false, error: 'Flow deletion not available' });
638
+ };
639
+
619
640
  module.exports = {
620
641
  createTemplate,
621
642
  listTemplates,
@@ -625,7 +646,6 @@ module.exports = {
625
646
  checkApprovalStatus,
626
647
  deleteTemplate,
627
648
  getCompleteTemplate,
628
- // Flow functions from templateFlowController
629
649
  createFlow,
630
650
  deleteFlow
631
651
  };
@@ -64,13 +64,64 @@ const createRouter = (routeDefinitions, controllers) => {
64
64
  return router;
65
65
  };
66
66
 
67
- // Helper function to setup all default routes with provided controllers
68
- const setupDefaultRoutes = (app, controllers) => {
69
- app.use('/api/assistant', createRouter(assistantRouteDefinitions, controllers));
70
- app.use('/api/conversation', createRouter(conversationRouteDefinitions, controllers));
71
- app.use('/api/media', createRouter(mediaRouteDefinitions, controllers));
72
- app.use('/api/message', createRouter(messageRouteDefinitions, controllers));
73
- app.use('/api/template', createRouter(templateRouteDefinitions, controllers));
67
+ // Import built-in controllers
68
+ const assistantController = require('../controllers/assistantController');
69
+ const conversationController = require('../controllers/conversationController');
70
+ const mediaController = require('../controllers/mediaController');
71
+ const messageController = require('../controllers/messageController');
72
+ const templateController = require('../controllers/templateController');
73
+
74
+ // Built-in controllers mapping
75
+ const builtInControllers = {
76
+ // Assistant controllers
77
+ activeAssistantController: assistantController.activeAssistantController,
78
+ addInsAssistantController: assistantController.addInsAssistantController,
79
+ addMsgAssistantController: assistantController.addMsgAssistantController,
80
+ createAssistantController: assistantController.createAssistantController,
81
+ getInfoAssistantController: assistantController.getInfoAssistantController,
82
+ listAssistantController: assistantController.listAssistantController,
83
+ switchAssistantController: assistantController.switchAssistantController,
84
+ stopAssistantController: assistantController.stopAssistantController,
85
+
86
+ // Conversation controllers
87
+ getConversationController: conversationController.getConversationController,
88
+ searchConversationsController: conversationController.searchConversationsController,
89
+ getConversationsByNameController: conversationController.getConversationsByNameController,
90
+ getConversationMessagesController: conversationController.getConversationMessagesController,
91
+ getNewMessagesController: conversationController.getNewMessagesController,
92
+ getConversationReplyController: conversationController.getConversationReplyController,
93
+ sendTemplateToNewNumberController: conversationController.sendTemplateToNewNumberController,
94
+ markMessagesAsReadController: conversationController.markMessagesAsReadController,
95
+
96
+ // Media controllers
97
+ getMediaController: mediaController.getMediaController,
98
+ handleFileUpload: mediaController.handleFileUpload,
99
+
100
+ // Message controllers
101
+ sendMessageController: messageController.sendMessageController,
102
+ sendBulkMessageController: messageController.sendBulkMessageController,
103
+ sendBulkMessageAirtableController: messageController.sendBulkMessageAirtableController,
104
+
105
+ // Template controllers
106
+ createTemplate: templateController.createTemplate,
107
+ listTemplates: templateController.listTemplates,
108
+ getPredefinedTemplates: templateController.getPredefinedTemplates,
109
+ getTemplate: templateController.getTemplate,
110
+ getCompleteTemplate: templateController.getCompleteTemplate,
111
+ createFlow: templateController.createFlow,
112
+ deleteFlow: templateController.deleteFlow,
113
+ submitForApproval: templateController.submitForApproval,
114
+ checkApprovalStatus: templateController.checkApprovalStatus,
115
+ deleteTemplate: templateController.deleteTemplate
116
+ };
117
+
118
+ // Helper function to setup all default routes using built-in controllers
119
+ const setupDefaultRoutes = (app) => {
120
+ app.use('/api/assistant', createRouter(assistantRouteDefinitions, builtInControllers));
121
+ app.use('/api/conversation', createRouter(conversationRouteDefinitions, builtInControllers));
122
+ app.use('/api/media', createRouter(mediaRouteDefinitions, builtInControllers));
123
+ app.use('/api/message', createRouter(messageRouteDefinitions, builtInControllers));
124
+ app.use('/api/template', createRouter(templateRouteDefinitions, builtInControllers));
74
125
  };
75
126
 
76
127
  module.exports = {
@@ -0,0 +1,82 @@
1
+ const { airtable } = require('../config/airtableConfig');
2
+
3
+
4
+ async function addRecord(baseID, tableName, fields) {
5
+ try {
6
+ const base = airtable.base(baseID);
7
+ const record = await base(tableName).create(fields);
8
+ console.log('Record added at', tableName);
9
+ return record;
10
+ } catch (error) {
11
+ console.error('Error adding record:', error);
12
+ throw error;
13
+ }
14
+ }
15
+
16
+
17
+ async function getRecords(baseID, tableName) {
18
+ try {
19
+ const records = [];
20
+ const base = airtable.base(baseID);
21
+ await base(tableName).select({
22
+ maxRecords: 3
23
+ }).eachPage((pageRecords, fetchNextPage) => {
24
+ pageRecords.forEach(record => {
25
+ records.push(record.fields);
26
+ });
27
+ fetchNextPage();
28
+ });
29
+ return records;
30
+ } catch (error) {
31
+ console.error('Error fetching records:', error);
32
+ }
33
+ }
34
+
35
+
36
+ async function getRecordByFilter(baseID, tableName, filter, view = 'Grid view') {
37
+ try {
38
+ const records = [];
39
+ const base = airtable.base(baseID);
40
+ await base(tableName).select({
41
+ filterByFormula: `${filter}`,
42
+ view: view
43
+ }).eachPage((pageRecords, fetchNextPage) => {
44
+ pageRecords.forEach(record => {
45
+ records.push(record.fields);
46
+ });
47
+ fetchNextPage();
48
+ });
49
+ return records;
50
+ } catch (error) {
51
+ console.error(`Error fetching records by ${filter}:`, error);
52
+ }
53
+ }
54
+
55
+
56
+ async function updateRecordByFilter(baseID, tableName, filter, updateFields) {
57
+ try {
58
+ const base = airtable.base(baseID);
59
+ const updatedRecords = [];
60
+
61
+ await base(tableName).select({
62
+ filterByFormula: `${filter}`
63
+ }).eachPage(async (pageRecords, fetchNextPage) => {
64
+ for (const record of pageRecords) {
65
+ const updatedRecord = await base(tableName).update(record.id, updateFields);
66
+ updatedRecords.push(updatedRecord);
67
+ }
68
+ fetchNextPage();
69
+ });
70
+
71
+ return updatedRecords;
72
+ } catch (error) {
73
+ console.error(`Error updating records by ${filter}:`, error);
74
+ }
75
+ }
76
+
77
+ module.exports = {
78
+ addRecord,
79
+ getRecords,
80
+ getRecordByFilter,
81
+ updateRecordByFilter
82
+ };
@@ -0,0 +1,55 @@
1
+ const { Calendar_ID } = require('../config/airtableConfig.js');
2
+
3
+ const { ISO_DATE, parseStartTime, addDays } = require('../utils/dateUtils.js');
4
+ const { dateAndTimeFromStart } = require('../utils/dateUtils.js');
5
+
6
+ const { getRecordByFilter, updateRecordByFilter } = require('../services/airtableService.js');
7
+
8
+
9
+ const getNextAppointmentForPatient = async (name, after = new Date()) => {
10
+ const filter = `AND(patient_name = "${name}", start_time >= '${ISO_DATE(after)}')`;
11
+ const rows = await getRecordByFilter(Calendar_ID, 'calendar_quimio', filter);
12
+ if (!rows?.length) return null;
13
+ return rows.sort((a, b) => new Date(a.start_time) - new Date(b.start_time))[0];
14
+ };
15
+
16
+ const getAppointmentsBetween = async (start, end) => {
17
+ const filter = `AND(start_time >= '${ISO_DATE(start)}', start_time < '${ISO_DATE(end)}')`;
18
+ return getRecordByFilter(Calendar_ID, 'calendar_quimio', filter);
19
+ };
20
+
21
+ const updateAppointmentById = async (recordId, data) => {
22
+ return updateRecordByFilter(
23
+ Calendar_ID,
24
+ 'calendar_quimio',
25
+ `REGEX_MATCH({record_id}, '${recordId}')`,
26
+ data
27
+ );
28
+ };
29
+
30
+ const buildAvailabilityWindow = async originalDate => {
31
+ const start = addDays(originalDate, 1);
32
+ const end = addDays(originalDate, 7);
33
+
34
+ const allSlots = await getAppointmentsBetween(start, addDays(end, 1));
35
+ const availableSlots = allSlots.filter(row => !row.patient);
36
+
37
+ const result = {};
38
+ availableSlots.forEach(row => {
39
+ const { date, time } = dateAndTimeFromStart(row.start_time);
40
+ if (!result[date]) result[date] = [];
41
+ result[date].push({ time, availableSpots: 1 });
42
+ });
43
+
44
+ return result;
45
+ };
46
+
47
+ const parseStart = parseStartTime;
48
+
49
+ module.exports = {
50
+ getNextAppointmentForPatient,
51
+ getAppointmentsBetween,
52
+ updateAppointmentById,
53
+ parseStart,
54
+ buildAvailabilityWindow
55
+ };
@@ -0,0 +1,296 @@
1
+ const { Historial_Clinico_ID } = require('../config/airtableConfig.js');
2
+ const { SALES_ASST, QUMIO_REMINDERS_ASST, DOCTOR_SCHEDULE_ASST, PATIENT_SCHEDULE_ASST } = require('../config/assistantConfig.js');
3
+ const { openaiClient } = require('../config/llmConfig.js');
4
+
5
+ const { Message, formatTimestamp } = require('../models/messageModel.js');
6
+ const { Thread } = require('../models/threadModel.js');
7
+
8
+ const { checkRunStatus, getCurRow } = require('../helpers/assistantHelper.js');
9
+ const { processMessage, getLastMessages } = require('../helpers/assistantHelper.js');
10
+ const { delay } = require('../helpers/whatsappHelper.js');
11
+
12
+ const { GeneralAssistant } = require('../assistants/generalAssistant.js');
13
+ const { SalesAssistant } = require('../assistants/salesAssistant.js');
14
+ const { QumioRemindersAssistant } = require('../assistants/qumioRemindersAssistant.js');
15
+ const { DoctorScheduleAssistant } = require('../assistants/doctorScheduleAssistant.js');
16
+ const { PatientScheduleAssistant } = require('../assistants/patientScheduleAssistant.js');
17
+
18
+
19
+ const getAssistantById = (assistant_id, thread) => {
20
+ switch (assistant_id) {
21
+ case SALES_ASST:
22
+ return new SalesAssistant(thread);
23
+ case QUMIO_REMINDERS_ASST:
24
+ return new QumioRemindersAssistant(thread);
25
+ case DOCTOR_SCHEDULE_ASST:
26
+ return new DoctorScheduleAssistant(thread);
27
+ case PATIENT_SCHEDULE_ASST:
28
+ return new PatientScheduleAssistant(thread);
29
+ default:
30
+ return new GeneralAssistant(thread);
31
+ }
32
+ };
33
+
34
+
35
+ const createAssistant = async (code, assistant_id, messages=[], prevThread=null) => {
36
+ // If thread already exists, update it
37
+ const findThread = await Thread.findOne({ code: code });
38
+ if (findThread) {
39
+ await Thread.updateOne({ code: code }, { $set: { active: true, stopped: false, assistant_id: assistant_id } });
40
+ return findThread;
41
+ }
42
+
43
+ const curRow = await getCurRow(Historial_Clinico_ID, code);
44
+ console.log('curRow', curRow[0]);
45
+ const nombre = curRow?.[0]?.['name'] || null;
46
+ const patientId = curRow?.[0]?.['record_id'] || null;
47
+
48
+ const assistant = getAssistantById(assistant_id, null);
49
+ const initialThread = await assistant.create(code, curRow[0]);
50
+
51
+ // Add new messages to memory
52
+ for (const message of messages) {
53
+ await openaiClient.beta.threads.messages.create(
54
+ initialThread.id, { role: 'assistant', content: message }
55
+ );
56
+ }
57
+
58
+ // Define new thread data
59
+ const thread = {
60
+ code: code,
61
+ assistant_id: assistant_id,
62
+ thread_id: initialThread.id,
63
+ patient_id: patientId,
64
+ run_id: null,
65
+ nombre: nombre,
66
+ active: true
67
+ };
68
+
69
+ const condition = { thread_id: prevThread?.thread_id };
70
+ const options = { new: true, upsert: true };
71
+ const updatedThread = await Thread.findOneAndUpdate(condition, thread, options);
72
+ console.log('Updated thread:', updatedThread);
73
+
74
+ // Delete previous thread
75
+ if (prevThread) {
76
+ await openaiClient.beta.threads.del(prevThread.thread_id);
77
+ }
78
+
79
+ return thread;
80
+ };
81
+
82
+ const addMsgAssistant = async (code, inMessages, reply = false) => {
83
+ try {
84
+ const thread = await Thread.findOne({ code: code });
85
+ console.log(thread);
86
+ if (thread === null) return null;
87
+
88
+ for (const message of inMessages) {
89
+ console.log(message);
90
+ await openaiClient.beta.threads.messages.create(
91
+ thread.thread_id, { role: 'assistant', content: message }
92
+ );
93
+ }
94
+
95
+ if (!reply) return null;
96
+
97
+ const assistant = getAssistantById(thread.assistant_id, thread);
98
+ const run = await openaiClient.beta.threads.runs.create(
99
+ thread.thread_id,
100
+ {
101
+ assistant_id: thread.assistant_id
102
+ }
103
+ );
104
+
105
+ await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: run.id } });
106
+ await checkRunStatus(assistant, run.thread_id, run.id);
107
+ await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: null } });
108
+
109
+ const messages = await openaiClient.beta.threads.messages.list(run.thread_id, { run_id: run.id });
110
+ const ans = messages.data[0].content[0].text.value;
111
+ console.log('THE ANS IS', ans);
112
+
113
+ return ans;
114
+ } catch (error) {
115
+ console.log(error);
116
+ return null;
117
+ }
118
+ };
119
+
120
+ const addInsAssistant = async (code, instruction) => {
121
+ try {
122
+ const thread = await Thread.findOne({ code: code });
123
+ console.log(thread);
124
+ if (thread === null) return null;
125
+
126
+ const assistant = getAssistantById(thread.assistant_id, thread);
127
+ const run = await openaiClient.beta.threads.runs.create(
128
+ thread.thread_id, {
129
+ assistant_id: thread.assistant_id,
130
+ additional_instructions: instruction,
131
+ additional_messages: [
132
+ { role: 'user', content: instruction }
133
+ ]
134
+ }
135
+ );
136
+
137
+ await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: run.id } });
138
+ await checkRunStatus(assistant, run.thread_id, run.id);
139
+ await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: null } });
140
+
141
+ const messages = await openaiClient.beta.threads.messages.list(run.thread_id, { run_id: run.id });
142
+ console.log(messages.data[0].content);
143
+ const ans = messages.data[0].content[0].text.value;
144
+
145
+ return ans;
146
+ } catch (error) {
147
+ console.log(error);
148
+ return null;
149
+ }
150
+ };
151
+
152
+ const getThread = async (code, message = null) => {
153
+ try {
154
+ let thread = await Thread.findOne({ code: code });
155
+ console.log('GET THREAD');
156
+ console.log(thread);
157
+
158
+ if (thread === null) {
159
+ if (message != null) {
160
+ const timestamp = formatTimestamp(message.messageTimestamp);
161
+ await Message.updateOne({ message_id: message.key.id, timestamp: timestamp }, { $set: { processed: true } });
162
+
163
+ }
164
+ return null;
165
+ }
166
+
167
+ while (thread && thread.run_id) {
168
+ console.log(`Wait for ${thread.run_id} to be executed`);
169
+ const run = await openaiClient.beta.threads.runs.retrieve(thread.thread_id, thread.run_id);
170
+ if (run.status === 'cancelled' || run.status === 'expired' || run.status === 'completed') {
171
+ await Thread.updateOne({ code: code }, { $set: { run_id: null } });
172
+ }
173
+ thread = await Thread.findOne({ code: code, active: true });
174
+ await delay(5000);
175
+ }
176
+
177
+ return thread;
178
+ } catch (error) {
179
+ console.error('Error in getThread:', error.message || error);
180
+ return null;
181
+ }
182
+ };
183
+
184
+ const getThreadInfo = async (code) => {
185
+ try {
186
+ let thread = await Thread.findOne({ code: code, active: true });
187
+ return thread;
188
+ } catch (error) {
189
+ console.log(error);
190
+ return null;
191
+ }
192
+ };
193
+
194
+ const replyAssistant = async function (code, message_ = null, thread_ = null, runOptions = {}) {
195
+ try {
196
+ let thread = thread_ || await getThread(code);
197
+ console.log('THREAD STOPPED', code, thread?.active);
198
+ if (!thread || !thread.active) return null;
199
+
200
+ const patientReply = message_ ? [message_] : await getLastMessages(code);
201
+ console.log('UNREAD DATA', patientReply);
202
+ if (!patientReply) {
203
+ console.log('No relevant data found for this assistant.');
204
+ return null;
205
+ }
206
+
207
+ let activeRuns = await openaiClient.beta.threads.runs.list(thread.thread_id);
208
+ console.log('ACTIVE RUNS:', activeRuns.length);
209
+ while (activeRuns.length > 0) {
210
+ console.log(`ACTIVE RUNS ${thread.thread_id}`);
211
+ activeRuns = await openaiClient.beta.threads.runs.list(thread.thread_id);
212
+ await delay(5000);
213
+ }
214
+
215
+ let patientMsg = false;
216
+ let urls = [];
217
+ for (const reply of patientReply) {
218
+ const { isNotAssistant, url } = await processMessage(code, reply, thread);
219
+ console.log(`isNotAssistant ${isNotAssistant} ${url}`);
220
+ patientMsg = patientMsg || isNotAssistant;
221
+ if (url) urls.push({ 'url': url });
222
+ }
223
+
224
+ if (urls.length > 0) {
225
+ console.log('urls', urls);
226
+ /*for (const url of urls) {
227
+ console.log("url", url);
228
+ await addRecord(Monitoreo_ID, 'estudios', [{"fields": {"estudios": urls,
229
+ "combined_estudios": [ { "url": url} ], "patient_id": [thread.patient_id]}}]);
230
+ }
231
+ const { pdfBuffer, processedFiles } = await combineImagesToPDF(code);
232
+ console.log("AFTER COMBINED IN BUFFER", processedFiles);
233
+ const key = `${code}-${Date.now()}-combined.pdf`;
234
+ await AWS.uploadBufferToS3(pdfBuffer, bucketName, key, "application/pdf");
235
+ const url = await AWS.generatePresignedUrl(bucketName, key);
236
+ console.log("New record", {"estudios": urls, "combined_estudios":
237
+ [ { "url": url} ], "patient_id": [thread.patient_id]});
238
+ await addRecord(Monitoreo_ID, 'estudios', [{"fields": {"estudios": urls,
239
+ "combined_estudios": [ { "url": url} ], "patient_id": [thread.patient_id]}}]);
240
+ await cleanupFiles(processedFiles);*/
241
+ }
242
+
243
+ thread = await getThread(code);
244
+ console.log('THREAD STOPPED', code, thread?.stopped);
245
+ if (!patientMsg || !thread || thread?.stopped) return null;
246
+
247
+ const assistant = getAssistantById(thread.assistant_id, thread);
248
+ const run = await openaiClient.beta.threads.runs.create(
249
+ thread.thread_id,
250
+ {
251
+ assistant_id: thread.assistant_id,
252
+ ...runOptions
253
+ }
254
+ );
255
+ console.log('RUN LAST ERROR:', run.last_error);
256
+
257
+ assistant.set_replies(patientReply);
258
+
259
+ await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: run.id } });
260
+ const runStatus = await checkRunStatus(assistant, run.thread_id, run.id);
261
+ console.log('RUN STATUS', runStatus);
262
+ await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: null } });
263
+
264
+ const messages = await openaiClient.beta.threads.messages.list(run.thread_id, { run_id: run.id });
265
+ const reply = messages.data?.[0]?.content?.[0]?.text?.value || '';
266
+ console.log(reply);
267
+
268
+ return reply;
269
+ } catch (err) {
270
+ console.log(`Error inside reply assistant ${err} ${code}`);
271
+ }
272
+ };
273
+
274
+ const switchAssistant = async (code, assistant_id) => {
275
+ try {
276
+ const thread = await Thread.findOne({ code: code });
277
+ console.log('Inside thread', thread);
278
+ if (thread === null) return;
279
+
280
+ await Thread.updateOne({ code }, { $set: { assistant_id: assistant_id, active: true, stopped: false } });
281
+ } catch (error) {
282
+ console.log(error);
283
+ return null;
284
+ }
285
+ };
286
+
287
+ module.exports = {
288
+ getThread,
289
+ getThreadInfo,
290
+ getAssistantById,
291
+ createAssistant,
292
+ replyAssistant,
293
+ addMsgAssistant,
294
+ addInsAssistant,
295
+ switchAssistant
296
+ };
@@ -0,0 +1,274 @@
1
+ const { Message } = require('../models/messageModel');
2
+ const { Historial_Clinico_ID } = require('../config/airtableConfig');
3
+ const { getRecordByFilter } = require('./airtableService');
4
+
5
+
6
+ const fetchConversationData = async (filter, skip, limit) => {
7
+ let filterConditions = { is_group: false };
8
+
9
+ switch (filter) {
10
+ case 'unread':
11
+ filterConditions = {
12
+ is_group: false,
13
+ from_me: false,
14
+ $or: [
15
+ { read: false },
16
+ { read: { $exists: false } }
17
+ ]
18
+ };
19
+ console.log('Applying unread filter');
20
+ break;
21
+
22
+ case 'no-response':
23
+ console.log('Applying no-response filter');
24
+ break;
25
+
26
+ case 'recent': {
27
+ const yesterday = new Date();
28
+ yesterday.setDate(yesterday.getDate() - 1);
29
+ filterConditions = {
30
+ is_group: false,
31
+ createdAt: { $gt: yesterday }
32
+ };
33
+ console.log('Applying recent filter (last 24 hours)');
34
+ break;
35
+ }
36
+
37
+ case 'all':
38
+ default:
39
+ filterConditions = { is_group: false };
40
+ console.log('Applying all conversations filter');
41
+ break;
42
+ }
43
+
44
+ console.log('Executing aggregation pipeline...');
45
+ const aggregationStartTime = Date.now();
46
+
47
+ let aggregationPipeline = [
48
+ { $match: filterConditions },
49
+ { $project: {
50
+ numero: 1,
51
+ body: 1,
52
+ createdAt: 1,
53
+ timestamp: 1,
54
+ is_media: 1,
55
+ media: 1,
56
+ nombre_whatsapp: 1,
57
+ from_me: 1
58
+ }},
59
+ { $group: {
60
+ _id: '$numero',
61
+ latestMessage: { $first: '$$ROOT' },
62
+ messageCount: { $sum: 1 }
63
+ }},
64
+ { $sort: { 'latestMessage.createdAt': -1 } }
65
+ ];
66
+
67
+ if (filter === 'no-response') {
68
+ aggregationPipeline.splice(-1, 0, { $match: { 'latestMessage.from_me': false } });
69
+ }
70
+
71
+ aggregationPipeline.push(
72
+ { $skip: skip },
73
+ { $limit: limit }
74
+ );
75
+
76
+ const conversations = await Message.aggregate(aggregationPipeline);
77
+
78
+ const aggregationTime = Date.now() - aggregationStartTime;
79
+ console.log(`Aggregation completed in ${aggregationTime}ms, found ${conversations.length} conversations`);
80
+
81
+ // Fetch names from Airtable and WhatsApp
82
+ const phoneNumbers = conversations.map(conv => conv._id).filter(Boolean);
83
+ const formula = 'OR(' +
84
+ phoneNumbers.map(p => `{whatsapp_id} = "${p}"`).join(', ') +
85
+ ')';
86
+ const patientTable = await getRecordByFilter(Historial_Clinico_ID, 'estado_general', formula);
87
+ const airtableNameMap = patientTable.reduce((map, patient) => {
88
+ map[patient.whatsapp_id] = patient.name;
89
+ return map;
90
+ }, {});
91
+ console.log(`Found ${Object.keys(airtableNameMap).length} names in Airtable`);
92
+
93
+ const contactNames = await Message.aggregate([
94
+ { $match: { is_group: false, from_me: false } },
95
+ { $sort: { createdAt: -1 } },
96
+ { $group: {
97
+ _id: '$numero',
98
+ name: { $first: '$nombre_whatsapp' }
99
+ }}
100
+ ]);
101
+
102
+ const nameMap = contactNames?.reduce((map, contact) => {
103
+ if (contact && contact._id) {
104
+ if (!airtableNameMap[contact._id]) {
105
+ map[contact._id] = contact.name || 'Unknown';
106
+ }
107
+ }
108
+ return map;
109
+ }, {...airtableNameMap}) || airtableNameMap || {};
110
+
111
+ // Fetch unread counts
112
+ console.log('Fetching unread counts using Message.aggregate');
113
+ const unreadCounts = await Message.aggregate([
114
+ {
115
+ $match: {
116
+ is_group: false,
117
+ from_me: false,
118
+ $or: [
119
+ { read: false },
120
+ { read: { $exists: false } }
121
+ ]
122
+ }
123
+ },
124
+ { $group: {
125
+ _id: '$numero',
126
+ unreadCount: { $sum: 1 }
127
+ }}
128
+ ]);
129
+
130
+ const unreadMap = unreadCounts?.reduce((map, item) => {
131
+ if (item && item._id) {
132
+ map[item._id] = item.unreadCount || 0;
133
+ }
134
+ return map;
135
+ }, {}) || {};
136
+ console.log('unreadMap', JSON.stringify(unreadMap));
137
+ console.log('Number of conversations found:', conversations?.length || 0);
138
+
139
+ // Calculate total count for pagination
140
+ let totalFilterConditions = { is_group: false };
141
+
142
+ if (filter === 'unread') {
143
+ totalFilterConditions = {
144
+ is_group: false,
145
+ from_me: false,
146
+ $or: [
147
+ { read: false },
148
+ { read: { $exists: false } }
149
+ ]
150
+ };
151
+ } else if (filter === 'recent') {
152
+ const yesterday = new Date();
153
+ yesterday.setDate(yesterday.getDate() - 1);
154
+ totalFilterConditions = {
155
+ is_group: false,
156
+ createdAt: { $gt: yesterday }
157
+ };
158
+ }
159
+
160
+ let totalAggregationPipeline = [
161
+ { $match: totalFilterConditions },
162
+ { $group: { _id: '$numero' } },
163
+ { $count: 'total' }
164
+ ];
165
+
166
+ if (filter === 'no-response') {
167
+ totalAggregationPipeline = [
168
+ { $match: { is_group: false } },
169
+ { $project: {
170
+ numero: 1,
171
+ from_me: 1,
172
+ createdAt: 1
173
+ }},
174
+ { $group: {
175
+ _id: '$numero',
176
+ latestMessage: { $first: '$$ROOT' }
177
+ }},
178
+ { $match: { 'latestMessage.from_me': false } },
179
+ { $count: 'total' }
180
+ ];
181
+ }
182
+
183
+ const totalConversations = await Message.aggregate(totalAggregationPipeline, { allowDiskUse: true });
184
+ const total = totalConversations[0]?.total || 0;
185
+
186
+ return { conversations, total, nameMap, unreadMap };
187
+ };
188
+
189
+ /**
190
+ * Processes conversations to prepare them for the response
191
+ */
192
+ const processConversations = async (conversations, nameMap, unreadMap) => {
193
+ console.log('Processing conversations for response...');
194
+
195
+ let processedConversations = [];
196
+ try {
197
+ processedConversations = (conversations || []).map((conv, index) => {
198
+ try {
199
+ if (!conv || !conv.latestMessage) {
200
+ console.warn(`Conversation ${index} missing latestMessage:`, conv?._id || 'unknown');
201
+ return {
202
+ phoneNumber: conv?._id || 'unknown',
203
+ name: 'Unknown',
204
+ lastMessage: '',
205
+ lastMessageTime: new Date(),
206
+ messageCount: 0,
207
+ unreadCount: 0,
208
+ isLastMessageMedia: false,
209
+ lastMessageType: null,
210
+ lastMessageFromMe: false
211
+ };
212
+ }
213
+
214
+ const isMedia = conv.latestMessage.is_media === true;
215
+ let mediaType = null;
216
+
217
+ if (isMedia && conv?.latestMessage?.media) {
218
+ if (conv.latestMessage.media.mediaType) {
219
+ mediaType = conv.latestMessage.media.mediaType;
220
+ } else if (conv.latestMessage.media.contentType) {
221
+ const contentType = conv.latestMessage.media.contentType;
222
+ const contentTypeParts = contentType?.split('/') || ['unknown'];
223
+ mediaType = contentTypeParts[0] || 'unknown';
224
+
225
+ if (mediaType === 'application') {
226
+ mediaType = 'document';
227
+ } else if (contentTypeParts[1] === 'webp') {
228
+ mediaType = 'sticker';
229
+ }
230
+ }
231
+ }
232
+
233
+ return {
234
+ phoneNumber: conv._id,
235
+ name: nameMap[conv._id] || conv?.latestMessage?.nombre_whatsapp || 'Unknown',
236
+ lastMessage: conv?.latestMessage?.body || '',
237
+ lastMessageTime: conv?.latestMessage?.createdAt || conv?.latestMessage?.timestamp || new Date(),
238
+ messageCount: conv.messageCount || 0,
239
+ unreadCount: unreadMap[conv._id] || 0,
240
+ isLastMessageMedia: isMedia || false,
241
+ lastMessageType: mediaType || null,
242
+ lastMessageFromMe: conv?.latestMessage?.from_me || false
243
+ };
244
+ } catch (convError) {
245
+ console.error(`Error processing conversation ${index}:`, convError);
246
+ return {
247
+ phoneNumber: conv?._id || `error_${index}`,
248
+ name: 'Error Processing',
249
+ lastMessage: 'Error processing conversation',
250
+ lastMessageTime: new Date(),
251
+ messageCount: 0,
252
+ unreadCount: 0,
253
+ isLastMessageMedia: false,
254
+ lastMessageType: null,
255
+ lastMessageFromMe: false
256
+ };
257
+ }
258
+ });
259
+
260
+ console.log(`Successfully processed ${processedConversations.length} conversations`);
261
+
262
+ } catch (mappingError) {
263
+ console.error('Error in conversation mapping:', mappingError);
264
+ // Return empty conversations if mapping fails
265
+ processedConversations = [];
266
+ }
267
+
268
+ return processedConversations;
269
+ };
270
+
271
+ module.exports = {
272
+ fetchConversationData,
273
+ processConversations
274
+ };
@@ -0,0 +1,23 @@
1
+ let sendMessage;
2
+ let sendScheduledMessage;
3
+ if (process.env.MESSAGING_PROVIDER === 'baileys') {
4
+ const baileyAdapter = require('../adapters/baileys');
5
+ console.log('Baileys adapter:', baileyAdapter);
6
+ console.log('Has sendMessage?', 'sendMessage' in baileyAdapter);
7
+ sendMessage = baileyAdapter.sendMessage;
8
+ sendScheduledMessage = baileyAdapter.sendScheduledMessage;
9
+ } else if (process.env.MESSAGING_PROVIDER === 'twilio') {
10
+ const { sendMessage: twilioSendMessage } = require('../messaging/messageService');
11
+ const { sendScheduledMessage: twilioSendScheduledMessage } = require('../messaging/scheduledMessageService');
12
+ const twilioAdapter = require('../adapters/twilio');
13
+ const twilioClient = twilioAdapter.twilioClient;
14
+ sendMessage = (messageData) => twilioSendMessage(twilioClient, messageData);
15
+ sendScheduledMessage = (messageData) => twilioSendScheduledMessage(twilioClient, messageData);
16
+ } else {
17
+ throw new Error('Unsupported MESSAGING_PROVIDER specified');
18
+ }
19
+
20
+ module.exports = {
21
+ sendMessage,
22
+ sendScheduledMessage
23
+ };
@@ -2,34 +2,10 @@ const AssistantManager = require('./AssistantManager');
2
2
  const DefaultLLMProvider = require('./DefaultLLMProvider');
3
3
  const MessageParser = require('./MessageParser');
4
4
  const logger = require('./logger');
5
- const { useMongoDBAuthState } = require('./mongoAuthConfig');
6
- const {
7
- convertTwilioToInternalFormat,
8
- downloadMediaFromTwilio,
9
- getMediaTypeFromContentType,
10
- extractTitle,
11
- ensureWhatsAppFormat
12
- } = require('./twilioHelper');
13
- const {
14
- delay,
15
- formatCode,
16
- calculateDelay
17
- } = require('./whatsappHelper');
18
5
 
19
6
  module.exports = {
20
7
  AssistantManager,
21
8
  DefaultLLMProvider,
22
9
  MessageParser,
23
- logger,
24
- useMongoDBAuthState,
25
- // Twilio utilities
26
- convertTwilioToInternalFormat,
27
- downloadMediaFromTwilio,
28
- getMediaTypeFromContentType,
29
- extractTitle,
30
- ensureWhatsAppFormat,
31
- // WhatsApp utilities
32
- delay,
33
- formatCode,
34
- calculateDelay
10
+ logger
35
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,7 +35,7 @@
35
35
  "dev": "tsc --watch",
36
36
  "test": "jest",
37
37
  "lint": "eslint lib/**/*.js",
38
- "prepublishOnly": "npm run lint",
38
+ "prepublishOnly": "npm test && npm run lint",
39
39
  "version": "npm run prepublishOnly && git add -A lib",
40
40
  "postversion": "git push && git push --tags"
41
41
  },
@@ -50,7 +50,9 @@
50
50
  "author": "PEOPL Health Tech",
51
51
  "license": "MIT",
52
52
  "dependencies": {
53
+ "airtable": "^0.12.2",
53
54
  "axios": "^1.5.0",
55
+ "dotenv": "^16.4.7",
54
56
  "moment-timezone": "^0.5.43",
55
57
  "mongoose": "^7.5.0",
56
58
  "pino": "^8.15.0",
@@ -1,75 +0,0 @@
1
- const axios = require('axios');
2
- const { v4: uuidv4 } = require('uuid');
3
-
4
- function convertTwilioToInternalFormat(twilioMessage) {
5
- const from = twilioMessage.From || '';
6
- const to = twilioMessage.To || '';
7
- const fromMe = to === from;
8
-
9
- return {
10
- key: {
11
- id: twilioMessage.MessageSid || uuidv4(),
12
- fromMe: fromMe,
13
- remoteJid: fromMe ? to : from
14
- },
15
- pushName: twilioMessage.ProfileName || '',
16
- message: {
17
- conversation: twilioMessage.Body || ''
18
- },
19
- messageTimestamp: Math.floor(Date.now() / 1000)
20
- };
21
- }
22
-
23
- async function downloadMediaFromTwilio(mediaUrl, credentials) {
24
- try {
25
- const response = await axios({
26
- method: 'GET',
27
- url: mediaUrl,
28
- responseType: 'arraybuffer',
29
- headers: {
30
- 'Authorization': `Basic ${Buffer.from(
31
- `${credentials.accountSid}:${credentials.authToken}`
32
- ).toString('base64')}`
33
- }
34
- });
35
-
36
- return Buffer.from(response.data);
37
- } catch (error) {
38
- console.error(`Failed to download media: ${error.message}`);
39
- throw error;
40
- }
41
- }
42
-
43
- function getMediaTypeFromContentType(contentType) {
44
- if (contentType.startsWith('image/')) return 'imageMessage';
45
- if (contentType.startsWith('audio/')) return 'audioMessage';
46
- if (contentType.startsWith('video/')) return 'videoMessage';
47
- if (contentType.startsWith('application/')) return 'documentMessage';
48
- return 'unknownMessage';
49
- }
50
-
51
- function extractTitle(message, mediaType) {
52
- if (mediaType === 'documentMessage' && message.MediaUrl0) {
53
- const urlParts = message.MediaUrl0.split('/');
54
- return urlParts[urlParts.length - 1] || null;
55
- }
56
- return null;
57
- }
58
-
59
- const ensureWhatsAppFormat = (phoneNumber) => {
60
- if (!phoneNumber) return null;
61
-
62
- if (phoneNumber.startsWith('whatsapp:')) {
63
- return phoneNumber;
64
- }
65
-
66
- return `whatsapp:${phoneNumber}`;
67
- };
68
-
69
- module.exports = {
70
- convertTwilioToInternalFormat,
71
- downloadMediaFromTwilio,
72
- getMediaTypeFromContentType,
73
- extractTitle,
74
- ensureWhatsAppFormat
75
- };
@@ -1,60 +0,0 @@
1
- const moment = require('moment-timezone');
2
-
3
- function delay(ms) {
4
- return new Promise(resolve => setTimeout(resolve, ms));
5
- }
6
-
7
- function formatCode(codeBase) {
8
- const [number, domain] = codeBase.split('@');
9
-
10
- if (!number || !domain) {
11
- throw new Error('Invalid code format: missing number or domain');
12
- }
13
-
14
- if (domain !== 's.whatsapp.net') {
15
- throw new Error('Invalid domain: must be s.whatsapp.net');
16
- }
17
-
18
- let formattedNumber = number;
19
-
20
- if (formattedNumber.endsWith('-2')) {
21
- formattedNumber = formattedNumber.slice(0, -2);
22
- }
23
-
24
- if (formattedNumber.length === 10) {
25
- formattedNumber = '52' + formattedNumber;
26
- }
27
-
28
- if (formattedNumber.startsWith('52') && formattedNumber.length === 12) {
29
- formattedNumber = formattedNumber.substring(0, 2) + '1' + formattedNumber.substring(2);
30
- }
31
- return `${formattedNumber}@s.whatsapp.net`;
32
- }
33
-
34
- function calculateDelay(sendTime, timeZone) {
35
- if (sendTime !== undefined && timeZone !== undefined) {
36
- const sendMoment = moment.tz(sendTime, timeZone);
37
-
38
- if (!sendMoment.isValid()) {
39
- return { error: 'Invalid time format' };
40
- }
41
-
42
- const now = moment().tz(timeZone);
43
- const randomDelay = Math.floor(Math.random() * 15001) + 15000;
44
- const delay = sendMoment.diff(now) + randomDelay;
45
-
46
- if (delay <= 0) {
47
- return 2500;
48
- }
49
-
50
- return delay;
51
- } else {
52
- return 2500;
53
- }
54
- }
55
-
56
- module.exports = {
57
- delay,
58
- formatCode,
59
- calculateDelay
60
- };