@peopl-health/nexus 1.2.0 → 1.3.1

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.
@@ -14,7 +14,10 @@ class BaileysProvider extends MessageProvider {
14
14
 
15
15
  async initialize() {
16
16
  try {
17
- const { default: makeWASocket, useMultiFileAuthState } = require('baileys');
17
+ const baileys = require('baileys');
18
+ // Support both CJS and ESM shapes
19
+ const makeWASocket = baileys.default || baileys.makeWASocket || baileys;
20
+ const useMultiFileAuthState = baileys.useMultiFileAuthState || baileys.useMultiFileAuthState;
18
21
  const { useMongoDBAuthState } = require('../config/mongoAuthConfig');
19
22
  const pino = require('pino');
20
23
 
@@ -175,6 +178,27 @@ class BaileysProvider extends MessageProvider {
175
178
  }
176
179
  this.isConnected = false;
177
180
  }
181
+
182
+ // Content/Template operations are not supported for Baileys
183
+ async listTemplates() {
184
+ throw new Error('Template operations are only supported with Twilio provider');
185
+ }
186
+ async getTemplate() {
187
+ throw new Error('Template operations are only supported with Twilio provider');
188
+ }
189
+ async checkApprovalStatus() {
190
+ throw new Error('Template operations are only supported with Twilio provider');
191
+ }
192
+ async submitForApproval() {
193
+ throw new Error('Template operations are only supported with Twilio provider');
194
+ }
195
+ async deleteTemplate() {
196
+ throw new Error('Template operations are only supported with Twilio provider');
197
+ }
198
+ async createTemplate() {
199
+ throw new Error('Template operations are only supported with Twilio provider');
200
+ }
201
+
178
202
  }
179
203
 
180
204
  module.exports = { BaileysProvider };
@@ -1,4 +1,5 @@
1
1
  const { MessageProvider } = require('../core/MessageProvider');
2
+ const axios = require('axios');
2
3
 
3
4
  /**
4
5
  * Twilio WhatsApp messaging provider
@@ -132,6 +133,112 @@ class TwilioProvider extends MessageProvider {
132
133
  throw new Error(`Failed to list templates: ${error.message}`);
133
134
  }
134
135
  }
136
+
137
+ /**
138
+ * Fetch a specific template/content by SID
139
+ */
140
+ async getTemplate(sid) {
141
+ if (!this.isConnected || !this.twilioClient) {
142
+ throw new Error('Twilio provider not initialized');
143
+ }
144
+ if (!sid) throw new Error('Content SID is required');
145
+ try {
146
+ const content = await this.twilioClient.content.v1.contents(sid).fetch();
147
+ return content;
148
+ } catch (error) {
149
+ throw new Error(`Failed to get template: ${error.message}`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Attempt to check approval status.
155
+ * Twilio SDK may not expose approval requests directly; return content and null approval by default.
156
+ */
157
+ async checkApprovalStatus(sid) {
158
+ if (!sid) throw new Error('Content SID is required');
159
+ const content = await this.getTemplate(sid);
160
+ try {
161
+ const links = (content && content.links) || {};
162
+ let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
163
+ if (!approvalsUrl) {
164
+ approvalsUrl = 'https://content.twilio.com/v1/Content/' + sid + '/ApprovalRequests';
165
+ }
166
+ const resp = await axios.get(approvalsUrl, { auth: { username: this.accountSid, password: this.authToken } });
167
+ const data = resp && resp.data ? resp.data : null;
168
+ let approvals = [];
169
+ if (data) {
170
+ approvals = data.approval_requests || data.approvalRequests || data.results || data.data || (Array.isArray(data) ? data : []);
171
+ if (!Array.isArray(approvals) && data.approvalRequest) approvals = [data.approvalRequest];
172
+ }
173
+ const approvalRequest = Array.isArray(approvals) && approvals.length > 0 ? approvals[0] : null;
174
+ return { content, approvalRequest };
175
+ } catch (error) {
176
+ // If approval endpoint unavailable, return content only
177
+ return { content, approvalRequest: null, warning: error.message };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Submit template for approval (best-effort placeholder)
183
+ */
184
+ async submitForApproval(contentSid, name, category) {
185
+ if (!contentSid) throw new Error('Content SID is required');
186
+ const content = await this.getTemplate(contentSid);
187
+ try {
188
+ const links = (content && content.links) || {};
189
+ let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
190
+ if (!approvalsUrl) {
191
+ approvalsUrl = 'https://content.twilio.com/v1/Content/' + contentSid + '/ApprovalRequests';
192
+ }
193
+ const payload = {
194
+ name,
195
+ category,
196
+ friendly_name: name,
197
+ categories: category ? [category] : undefined,
198
+ channel: 'whatsapp'
199
+ };
200
+ const resp = await axios.post(approvalsUrl, payload, { auth: { username: this.accountSid, password: this.authToken } });
201
+ return { success: true, contentSid, approvalRequest: resp.data || null };
202
+ } catch (error) {
203
+ throw new Error(`Failed to submit for approval: ${error.message}`);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Delete a template/content by SID
209
+ */
210
+ async deleteTemplate(sid) {
211
+ if (!this.isConnected || !this.twilioClient) {
212
+ throw new Error('Twilio provider not initialized');
213
+ }
214
+ if (!sid) throw new Error('Content SID is required');
215
+ try {
216
+ await this.twilioClient.content.v1.contents(sid).remove();
217
+ return { success: true };
218
+ } catch (error) {
219
+ throw new Error(`Failed to delete template: ${error.message}`);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Create a template/content using Twilio Content API
225
+ * @param {Object} templateData - Must follow Twilio Content API schema
226
+ */
227
+ async createTemplate(templateData) {
228
+ if (!this.isConnected || !this.twilioClient) {
229
+ throw new Error('Twilio provider not initialized');
230
+ }
231
+ if (!templateData || typeof templateData !== 'object') {
232
+ throw new Error('templateData must be an object');
233
+ }
234
+ try {
235
+ const created = await this.twilioClient.content.v1.contents.create(templateData);
236
+ return created;
237
+ } catch (error) {
238
+ throw new Error(`Failed to create template: ${error.message}`);
239
+ }
240
+ }
241
+
135
242
  }
136
243
 
137
244
  module.exports = { TwilioProvider };
@@ -0,0 +1,29 @@
1
+ const { TwilioProvider } = require('./TwilioProvider');
2
+ const { BaileysProvider } = require('./BaileysProvider');
3
+
4
+ const _providers = new Map();
5
+
6
+ function registerProvider(name, ProviderClass) {
7
+ if (!name || !ProviderClass) throw new Error('registerProvider requires name and ProviderClass');
8
+ _providers.set(String(name).toLowerCase(), ProviderClass);
9
+ }
10
+
11
+ function getProvider(name) {
12
+ return _providers.get(String(name || '').toLowerCase());
13
+ }
14
+
15
+ function createProvider(name, config) {
16
+ const ProviderClass = getProvider(name);
17
+ if (!ProviderClass) throw new Error(`Unsupported provider: ${name}`);
18
+ return new ProviderClass(config || {});
19
+ }
20
+
21
+ // Register built-ins
22
+ registerProvider('twilio', TwilioProvider);
23
+ registerProvider('baileys', BaileysProvider);
24
+
25
+ module.exports = {
26
+ registerProvider,
27
+ getProvider,
28
+ createProvider
29
+ };
@@ -0,0 +1,38 @@
1
+ const path = require('path');
2
+
3
+ const defaults = {
4
+ provider: { name: 'twilio', config: {} },
5
+ storage: null,
6
+ features: { airtable: true, s3: true },
7
+ assistants: { registry: {}, select: {}, getAssistantById: null },
8
+ handlers: {},
9
+ interactive: {}
10
+ };
11
+
12
+ function merge(a, b) {
13
+ if (!b) return { ...a };
14
+ const out = { ...a };
15
+ for (const k of Object.keys(b)) {
16
+ if (b[k] && typeof b[k] === 'object' && !Array.isArray(b[k])) {
17
+ out[k] = merge(a[k] || {}, b[k]);
18
+ } else {
19
+ out[k] = b[k];
20
+ }
21
+ }
22
+ return out;
23
+ }
24
+
25
+ function loadNexusConfig(userConfig) {
26
+ if (userConfig && typeof userConfig === 'object') {
27
+ return merge(defaults, userConfig);
28
+ }
29
+ try {
30
+ const localPath = path.resolve(process.cwd(), 'nexus.config.js');
31
+ const fileConfig = require(localPath);
32
+ return merge(defaults, fileConfig || {});
33
+ } catch {
34
+ return { ...defaults };
35
+ }
36
+ }
37
+
38
+ module.exports = { loadNexusConfig, defaults };
@@ -1,6 +1,6 @@
1
1
  const { Config_ID } = require('../config/airtableConfig');
2
2
 
3
- const { updateThreadActive, updateThreadStop, Thread } = require('../models/threadModel');
3
+ const { Thread } = require('../models/threadModel');
4
4
 
5
5
  const { getRecordByFilter } = require('../services/airtableService');
6
6
  const { createAssistant, addMsgAssistant, addInsAssistant } = require('../services/assistantService');
@@ -12,7 +12,7 @@ const activeAssistantController = async (req, res) => {
12
12
  const { code, active } = req.body;
13
13
 
14
14
  try {
15
- await updateThreadActive(code, active);
15
+ await Thread.updateOne({ code }, { $set: { active: !!active } });
16
16
  return res.status(200).send({ message: 'Active assistant' });
17
17
  } catch (error) {
18
18
  console.log(error);
@@ -34,7 +34,7 @@ const addInsAssistantController = async (req, res) => {
34
34
  };
35
35
 
36
36
  const addMsgAssistantController = async (req, res) => {
37
- const { code, messages, reply } = req.body;
37
+ const { code, messages, reply = false } = req.body;
38
38
 
39
39
  try {
40
40
  const ans = await addMsgAssistant(code, messages, reply);
@@ -47,7 +47,7 @@ const addMsgAssistantController = async (req, res) => {
47
47
  };
48
48
 
49
49
  const createAssistantController = async (req, res) => {
50
- const { assistant_id, codes, messages=[], force=false } = req.body;
50
+ const { assistant_id, codes, instrucciones=[], messages=[], force=false } = req.body;
51
51
  if (!Array.isArray(codes) || codes.length === 0) {
52
52
  return res.status(400).send({ error: 'codes must be a non-empty array' });
53
53
  }
@@ -62,7 +62,7 @@ const createAssistantController = async (req, res) => {
62
62
  if (!force) continue;
63
63
  }
64
64
 
65
- await createAssistant(code, assistant_id, messages, thread);
65
+ await createAssistant(code, assistant_id, [...instrucciones, ...messages], thread);
66
66
  console.log('messages', messages);
67
67
  for (const message of messages) {
68
68
  console.log('message', message);
@@ -114,7 +114,7 @@ const stopAssistantController = async (req, res) => {
114
114
  const { code, stop } = req.body;
115
115
 
116
116
  try {
117
- await updateThreadStop(code, stop);
117
+ await Thread.updateOne({ code }, { $set: { stopped: !!stop } });
118
118
  return res.status(200).send({ message: 'Stop assistant' });
119
119
  } catch (error) {
120
120
  console.log(error);
@@ -1,17 +1,21 @@
1
+ const { Message } = require('../models/messageModel.js');
2
+
1
3
  // Import from Nexus core
2
4
  const { sendMessage } = require('../core/NexusMessaging');
3
5
 
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' })
6
+ // Injectable dependencies with safe defaults
7
+ let injected = {
8
+ ScheduledMessage: null,
9
+ getRecordByFilter: null,
10
+ sendScheduledMessage: null
10
11
  };
11
12
 
12
- // Stub functions for missing services
13
- const getRecordByFilter = () => Promise.resolve(null);
14
- const sendScheduledMessage = () => Promise.resolve({ success: false, error: 'Service not available' });
13
+ // Allow consumers to inject their model and services
14
+ const configureMessageController = ({ ScheduledMessage, getRecordByFilter, sendScheduledMessage } = {}) => {
15
+ if (ScheduledMessage) injected.ScheduledMessage = ScheduledMessage;
16
+ if (getRecordByFilter) injected.getRecordByFilter = getRecordByFilter;
17
+ if (sendScheduledMessage) injected.sendScheduledMessage = sendScheduledMessage;
18
+ };
15
19
 
16
20
  const moment = require('moment-timezone');
17
21
 
@@ -31,6 +35,9 @@ const sendMessageController = async (req, res) => {
31
35
  const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 2500 : new Date();
32
36
 
33
37
  try {
38
+ if (!injected.ScheduledMessage || typeof injected.ScheduledMessage.create !== 'function') {
39
+ return res.status(500).json({ success: false, error: 'ScheduledMessage model not configured. Call configureMessageController() to inject it.' });
40
+ }
34
41
  const messageData = {
35
42
  fileUrl,
36
43
  message,
@@ -44,7 +51,7 @@ const sendMessageController = async (req, res) => {
44
51
  extraDelay: 0,
45
52
  variables
46
53
  };
47
- await ScheduledMessage.create(messageData);
54
+ await injected.ScheduledMessage.create(messageData);
48
55
  console.log('Sending message with data:', messageData);
49
56
 
50
57
  const result = await sendMessage(messageData);
@@ -76,12 +83,18 @@ const sendBulkMessageController = async (req, res) => {
76
83
  const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20*1000 : new Date();
77
84
 
78
85
  try {
86
+ if (!injected.ScheduledMessage || typeof injected.ScheduledMessage.create !== 'function') {
87
+ return res.status(500).json({ success: false, error: 'ScheduledMessage model not configured. Call configureMessageController() to inject it.' });
88
+ }
89
+ if (!injected.sendScheduledMessage || typeof injected.sendScheduledMessage !== 'function') {
90
+ return res.status(500).json({ success: false, error: 'sendScheduledMessage not configured. Call configureMessageController() to inject it.' });
91
+ }
79
92
  let numSend = 0;
80
93
  let extraDelay = 0;
81
94
  let curMessage = message;
82
95
  const scheduledMessages = [];
83
96
  for (const code of codes) {
84
- const scheduledMessage = new ScheduledMessage({
97
+ const scheduledMessage = new injected.ScheduledMessage({
85
98
  fileUrl,
86
99
  message: curMessage,
87
100
  fileType,
@@ -106,8 +119,8 @@ const sendBulkMessageController = async (req, res) => {
106
119
  // Schedule all messages with Agenda in parallel
107
120
  const sentMessages = await Promise.all(
108
121
  scheduledMessages.map(async (message) => {
109
- const savedMessage = await ScheduledMessage.create(message);
110
- return sendScheduledMessage(savedMessage);
122
+ const savedMessage = await injected.ScheduledMessage.create(message);
123
+ return injected.sendScheduledMessage(savedMessage);
111
124
  })
112
125
  );
113
126
 
@@ -153,7 +166,16 @@ const sendBulkMessageAirtableController = async (req, res) => {
153
166
  }
154
167
 
155
168
  try {
156
- const rows = await getRecordByFilter(baseId, tableName, condition);
169
+ if (!injected.getRecordByFilter || typeof injected.getRecordByFilter !== 'function') {
170
+ return res.status(500).json({ success: false, error: 'Airtable getRecordByFilter not configured. Call configureMessageController() to inject it.' });
171
+ }
172
+ if (!injected.ScheduledMessage || typeof injected.ScheduledMessage.create !== 'function') {
173
+ return res.status(500).json({ success: false, error: 'ScheduledMessage model not configured. Call configureMessageController() to inject it.' });
174
+ }
175
+ if (!injected.sendScheduledMessage || typeof injected.sendScheduledMessage !== 'function') {
176
+ return res.status(500).json({ success: false, error: 'sendScheduledMessage not configured. Call configureMessageController() to inject it.' });
177
+ }
178
+ const rows = await injected.getRecordByFilter(baseId, tableName, condition);
157
179
  let extraDelay = 0;
158
180
  let curMessage = message;
159
181
  const sentPhones = new Set();
@@ -192,18 +214,14 @@ const sendBulkMessageAirtableController = async (req, res) => {
192
214
  variables
193
215
  };
194
216
 
195
- // Add to scheduledMessages for both saving and scheduling
196
217
  scheduledMessages.push(scheduledMessage);
197
-
198
- // Increment delay and message counter
199
218
  extraDelay += Math.floor(Math.random() * 5001) + 5000;
200
219
  }
201
220
 
202
- // Schedule all messages with Agenda in parallel
203
221
  const sentMessages = await Promise.all(
204
222
  scheduledMessages.map(async (message) => {
205
- const savedMessage = await ScheduledMessage.create(message);
206
- return sendScheduledMessage(savedMessage);
223
+ const savedMessage = await injected.ScheduledMessage.create(message);
224
+ return injected.sendScheduledMessage(savedMessage);
207
225
  })
208
226
  );
209
227
 
@@ -223,8 +241,37 @@ const sendBulkMessageAirtableController = async (req, res) => {
223
241
  }
224
242
  };
225
243
 
244
+ const getLastInteractionController = async (req, res) => {
245
+ const { code } = req.body;
246
+
247
+ try {
248
+ const lastMessage = await Message.findOne({
249
+ $or: [
250
+ { numero: code },
251
+ { group_id: code }
252
+ ]
253
+ }).sort({ createdAt: -1 }).exec();
254
+
255
+ if (!lastMessage) {
256
+ return res.status(404).send({ message: 'No messages found for the provided code.' });
257
+ }
258
+
259
+ const createdAt = new Date(lastMessage.createdAt);
260
+ const now = new Date();
261
+ const timeDiffMs = now - createdAt;
262
+ const minutes = Math.floor(timeDiffMs / (1000 * 60));
263
+
264
+ return res.status(200).send({ message: 'Last interaction retrieved successfully.', lastMessage, minutes });
265
+ } catch (error) {
266
+ console.error(error);
267
+ return res.status(500).send({ message: 'Failed to retrieve the last interaction.', error });
268
+ }
269
+ };
270
+
226
271
  module.exports = {
227
272
  sendMessageController,
228
273
  sendBulkMessageController,
229
- sendBulkMessageAirtableController
274
+ sendBulkMessageAirtableController,
275
+ getLastInteractionController,
276
+ configureMessageController
230
277
  };
@@ -17,33 +17,21 @@ const checkTemplateSupport = () => {
17
17
  throw new Error('Template operations are only supported with Twilio provider');
18
18
  }
19
19
  };
20
- const mongoose = require('mongoose');
20
+
21
21
  const { handleApiError } = require('../utils/errorHandler');
22
22
 
23
23
  const { Template } = require('../templates/templateStructure');
24
24
  const predefinedTemplates = require('../templates/predefinedTemplates');
25
25
 
26
- // Use mongoose models to avoid conflicts
26
+
27
27
  const getTemplateModel = () => {
28
- if (mongoose.models.Template) {
29
- return mongoose.models.Template;
30
- }
31
-
32
- // If not in mongoose.models, require and return the model
28
+ // Require the concrete model; enforce presence instead of stubbing
33
29
  try {
30
+ // If already registered in mongoose.models, require still returns the same model
34
31
  const TemplateModel = require('../models/templateModel');
35
32
  return TemplateModel;
36
33
  } catch (error) {
37
- console.error('Failed to load Template model:', error);
38
- // Return a stub model with required methods
39
- return {
40
- deleteMany: () => Promise.resolve({ deletedCount: 0 }),
41
- find: () => ({ sort: () => ({ limit: () => ({ lean: () => Promise.resolve([]) }) }) }),
42
- findOne: () => Promise.resolve(null),
43
- create: () => Promise.resolve({}),
44
- updateOne: () => Promise.resolve({}),
45
- deleteOne: () => Promise.resolve({})
46
- };
34
+ throw new Error('Template model not available. Ensure models are loaded before using template controllers.');
47
35
  }
48
36
  };
49
37