@peopl-health/nexus 2.5.6-fix-switch → 2.5.7
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/lib/adapters/TwilioProvider.js +3 -9
- package/lib/controllers/conversationController.js +10 -16
- package/lib/controllers/messageController.js +8 -30
- package/lib/core/NexusMessaging.js +1 -3
- package/lib/helpers/baileysHelper.js +3 -3
- package/lib/helpers/processHelper.js +3 -3
- package/lib/helpers/twilioHelper.js +8 -7
- package/lib/index.d.ts +2 -2
- package/lib/models/messageModel.js +40 -73
- package/lib/services/assistantServiceCore.js +0 -4
- package/lib/services/conversationService.js +76 -245
- package/lib/storage/MongoStorage.js +25 -61
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ const { MessageProvider } = require('../core/MessageProvider');
|
|
|
2
2
|
const axios = require('axios');
|
|
3
3
|
const runtimeConfig = require('../config/runtimeConfig');
|
|
4
4
|
const { uploadMediaToS3, getFileExtension } = require('../helpers/mediaHelper');
|
|
5
|
+
const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
|
|
5
6
|
const { sanitizeMediaFilename } = require('../utils/sanitizer');
|
|
6
7
|
const { generatePresignedUrl } = require('../config/awsConfig');
|
|
7
8
|
const { validateMedia, getMediaType } = require('../utils/mediaValidator');
|
|
@@ -35,13 +36,6 @@ class TwilioProvider extends MessageProvider {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
ensureWhatsAppFormat(number) {
|
|
39
|
-
if (!number) return null;
|
|
40
|
-
if (number.startsWith('whatsapp:')) return number;
|
|
41
|
-
if (number.startsWith('+')) return `whatsapp:${number}`;
|
|
42
|
-
return `whatsapp:+${number}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
39
|
async sendMessage(messageData) {
|
|
46
40
|
if (!this.isConnected || !this.twilioClient) {
|
|
47
41
|
throw new Error('Twilio provider not initialized');
|
|
@@ -49,8 +43,8 @@ class TwilioProvider extends MessageProvider {
|
|
|
49
43
|
|
|
50
44
|
const { code, body, fileUrl, fileType, variables, contentSid } = messageData;
|
|
51
45
|
|
|
52
|
-
const formattedFrom =
|
|
53
|
-
const formattedCode =
|
|
46
|
+
const formattedFrom = ensureWhatsAppFormat(this.whatsappNumber);
|
|
47
|
+
const formattedCode = ensureWhatsAppFormat(code);
|
|
54
48
|
|
|
55
49
|
|
|
56
50
|
if (!formattedFrom || !formattedCode) {
|
|
@@ -3,6 +3,7 @@ const llmConfig = require('../config/llmConfig');
|
|
|
3
3
|
const { Historial_Clinico_ID } = require('../config/airtableConfig');
|
|
4
4
|
const { Thread } = require('../models/threadModel');
|
|
5
5
|
const { withThreadRecovery } = require('../helpers/threadRecoveryHelper');
|
|
6
|
+
const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
|
|
6
7
|
const { getRecordByFilter } = require('../services/airtableService');
|
|
7
8
|
const { fetchConversationData, processConversations } = require('../services/conversationService');
|
|
8
9
|
const { sendMessage } = require('../core/NexusMessaging');
|
|
@@ -107,7 +108,7 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
107
108
|
const limit = parseInt(req.query.limit) || 50;
|
|
108
109
|
const { before } = req.query;
|
|
109
110
|
|
|
110
|
-
const query = { numero: phoneNumber,
|
|
111
|
+
const query = { numero: phoneNumber, groud_id: null };
|
|
111
112
|
if (before) {
|
|
112
113
|
try {
|
|
113
114
|
query.createdAt = { $lt: new Date(before) };
|
|
@@ -139,7 +140,7 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
139
140
|
return true;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
if (msg.
|
|
143
|
+
if (msg.media) {
|
|
143
144
|
if (!msg.media || typeof msg.media !== 'object') {
|
|
144
145
|
logger.warn('Found media message with invalid media data:', msg._id);
|
|
145
146
|
return true;
|
|
@@ -180,7 +181,7 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
180
181
|
body: msg.body || '[Message content unavailable]',
|
|
181
182
|
createdAt: msg.createdAt || new Date(),
|
|
182
183
|
from_me: !!msg.from_me,
|
|
183
|
-
is_media: !!msg.
|
|
184
|
+
is_media: !!msg.media,
|
|
184
185
|
error: 'Message contained non-serializable data'
|
|
185
186
|
};
|
|
186
187
|
}
|
|
@@ -223,9 +224,7 @@ const getConversationReplyController = async (req, res) => {
|
|
|
223
224
|
});
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
const formattedPhoneNumber = phoneNumber
|
|
227
|
-
? phoneNumber
|
|
228
|
-
: `whatsapp:${phoneNumber}`;
|
|
227
|
+
const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
|
|
229
228
|
logger.info('Formatted phone number:', formattedPhoneNumber);
|
|
230
229
|
|
|
231
230
|
const messageData = {
|
|
@@ -315,7 +314,7 @@ const searchConversationsController = async (req, res) => {
|
|
|
315
314
|
// Search through all conversations in the database
|
|
316
315
|
const conversations = await Message.aggregate([
|
|
317
316
|
{ $match: {
|
|
318
|
-
|
|
317
|
+
group_id: null,
|
|
319
318
|
$or: [
|
|
320
319
|
{ numero: { $regex: escapedQuery, $options: 'i' } },
|
|
321
320
|
{ nombre_whatsapp: { $regex: escapedQuery, $options: 'i' } },
|
|
@@ -327,7 +326,6 @@ const searchConversationsController = async (req, res) => {
|
|
|
327
326
|
body: 1,
|
|
328
327
|
createdAt: 1,
|
|
329
328
|
timestamp: 1,
|
|
330
|
-
is_media: 1,
|
|
331
329
|
media: 1,
|
|
332
330
|
nombre_whatsapp: 1,
|
|
333
331
|
from_me: 1
|
|
@@ -403,7 +401,7 @@ const searchConversationsController = async (req, res) => {
|
|
|
403
401
|
};
|
|
404
402
|
}
|
|
405
403
|
|
|
406
|
-
const isMedia = conv.latestMessage.
|
|
404
|
+
const isMedia = !!conv.latestMessage.media;
|
|
407
405
|
let mediaType = null;
|
|
408
406
|
|
|
409
407
|
if (isMedia && conv?.latestMessage?.media) {
|
|
@@ -458,7 +456,7 @@ const searchConversationsController = async (req, res) => {
|
|
|
458
456
|
const getConversationsByNameController = async (req, res) => {
|
|
459
457
|
try {
|
|
460
458
|
const conversations = await Message.aggregate([
|
|
461
|
-
{ $match: { from_me: false,
|
|
459
|
+
{ $match: { from_me: false, groud_id: null } },
|
|
462
460
|
{ $sort: { createdAt: -1 } },
|
|
463
461
|
{ $group: {
|
|
464
462
|
_id: '$numero',
|
|
@@ -511,7 +509,7 @@ const getNewMessagesController = async (req, res) => {
|
|
|
511
509
|
|
|
512
510
|
const query = {
|
|
513
511
|
numero: phoneNumber,
|
|
514
|
-
|
|
512
|
+
group_id: null,
|
|
515
513
|
createdAt: { $gt: lastMessage.createdAt }
|
|
516
514
|
};
|
|
517
515
|
|
|
@@ -593,12 +591,8 @@ const sendTemplateToNewNumberController = async (req, res) => {
|
|
|
593
591
|
});
|
|
594
592
|
}
|
|
595
593
|
|
|
596
|
-
|
|
597
|
-
const formattedPhoneNumber = phoneNumber.startsWith('whatsapp:')
|
|
598
|
-
? phoneNumber
|
|
599
|
-
: `whatsapp:${phoneNumber}`;
|
|
594
|
+
const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
|
|
600
595
|
|
|
601
|
-
// Log template details for debugging
|
|
602
596
|
logger.info('Sending template to new number with details:', {
|
|
603
597
|
phoneNumber: formattedPhoneNumber,
|
|
604
598
|
templateId,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
1
2
|
const { Message } = require('../models/messageModel.js');
|
|
2
3
|
const { ScheduledMessage: DefaultScheduledMessage } = require('../models/agendaMessageModel.js');
|
|
3
4
|
const {
|
|
4
5
|
sendMessage: defaultSendMessage,
|
|
5
6
|
sendScheduledMessage: defaultSendScheduledMessage
|
|
6
7
|
} = require('../core/NexusMessaging');
|
|
8
|
+
const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
|
|
7
9
|
const { getRecordByFilter: defaultGetRecordByFilter } = require('../services/airtableService');
|
|
8
|
-
const runtimeConfig = require('../config/runtimeConfig');
|
|
9
10
|
const { logger } = require('../utils/logger');
|
|
10
11
|
const moment = require('moment-timezone');
|
|
11
12
|
|
|
@@ -58,29 +59,6 @@ const pickMessageId = (result, fallbackDoc) => {
|
|
|
58
59
|
return null;
|
|
59
60
|
};
|
|
60
61
|
|
|
61
|
-
const normalizeCode = (code) => {
|
|
62
|
-
if (!code || typeof code !== 'string') return code;
|
|
63
|
-
|
|
64
|
-
const trimmed = code.trim();
|
|
65
|
-
if (trimmed.startsWith('whatsapp:')) {
|
|
66
|
-
return trimmed;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (trimmed.includes('@')) {
|
|
70
|
-
return trimmed;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (trimmed.startsWith('+')) {
|
|
74
|
-
return `whatsapp:${trimmed}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (/^\d+$/.test(trimmed)) {
|
|
78
|
-
return `whatsapp:+${trimmed}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return trimmed;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
62
|
const sendMessageController = async (req, res) => {
|
|
85
63
|
const {
|
|
86
64
|
fileUrl,
|
|
@@ -112,7 +90,7 @@ const sendMessageController = async (req, res) => {
|
|
|
112
90
|
sendTime: sendMoment,
|
|
113
91
|
contentSid,
|
|
114
92
|
hidePreview,
|
|
115
|
-
code:
|
|
93
|
+
code: ensureWhatsAppFormat(code),
|
|
116
94
|
author,
|
|
117
95
|
extraDelay: 0,
|
|
118
96
|
variables
|
|
@@ -173,7 +151,7 @@ const sendBulkMessageController = async (req, res) => {
|
|
|
173
151
|
sendTime: new Date(sendMoment + extraDelay),
|
|
174
152
|
contentSid,
|
|
175
153
|
hidePreview,
|
|
176
|
-
code:
|
|
154
|
+
code: ensureWhatsAppFormat(recipient),
|
|
177
155
|
author,
|
|
178
156
|
extraDelay,
|
|
179
157
|
variables
|
|
@@ -269,7 +247,7 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
269
247
|
let code = row[columnPhone];
|
|
270
248
|
if (Array.isArray(code)) code = code[0];
|
|
271
249
|
if (!code) continue;
|
|
272
|
-
code =
|
|
250
|
+
code = ensureWhatsAppFormat(code);
|
|
273
251
|
|
|
274
252
|
if (sentPhones.has(code)) continue;
|
|
275
253
|
sentPhones.add(code);
|
|
@@ -323,7 +301,7 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
323
301
|
|
|
324
302
|
const getLastInteractionController = async (req, res) => {
|
|
325
303
|
const { code } = req.query;
|
|
326
|
-
const normalizedCode =
|
|
304
|
+
const normalizedCode = ensureWhatsAppFormat(code);
|
|
327
305
|
|
|
328
306
|
try {
|
|
329
307
|
const lastMessage = await Message.findOne({
|
|
@@ -355,7 +333,7 @@ const checkScheduledMessageStatusController = async (req, res) => {
|
|
|
355
333
|
if (!ensureDependency(res, dependencies.ScheduledMessage, 'ScheduledMessage model not configured.')) return;
|
|
356
334
|
|
|
357
335
|
try {
|
|
358
|
-
const msg = await dependencies.ScheduledMessage.findOne({ contentSid, code:
|
|
336
|
+
const msg = await dependencies.ScheduledMessage.findOne({ contentSid, code: ensureWhatsAppFormat(code) });
|
|
359
337
|
if (!msg) return res.status(404).send({ message: 'ScheduledMessage not found' });
|
|
360
338
|
return res.status(200).send({ message: 'ScheduledMessage status retrieved successfully.', sent: msg?.status === 'sent', status: msg?.status || null });
|
|
361
339
|
} catch (error) {
|
|
@@ -369,7 +347,7 @@ const checkMessageStatusController = async (req, res) => {
|
|
|
369
347
|
if (!contentSid || !code) return res.status(400).send({ message: 'contentSid and code are required' });
|
|
370
348
|
|
|
371
349
|
try {
|
|
372
|
-
const msg = await Message.findOne({ content_sid: contentSid, numero:
|
|
350
|
+
const msg = await Message.findOne({ content_sid: contentSid, numero: ensureWhatsAppFormat(code) });
|
|
373
351
|
if (!msg) return res.status(404).send({ message: 'Message not found' });
|
|
374
352
|
const sent = msg?.statusInfo?.status === 'sent' || msg?.statusInfo?.status === 'delivered' || msg?.statusInfo?.status === 'read';
|
|
375
353
|
return res.status(200).send({ message: 'Message status retrieved successfully.', sent, status: msg?.statusInfo?.status || null });
|
|
@@ -585,9 +585,7 @@ class NexusMessaging {
|
|
|
585
585
|
const messageObj = convertTwilioToInternalFormat(raw);
|
|
586
586
|
const values = getMessageValues(
|
|
587
587
|
messageObj,
|
|
588
|
-
messageData.body || messageData.caption || ''
|
|
589
|
-
null,
|
|
590
|
-
true
|
|
588
|
+
messageData.body || messageData.caption || ''
|
|
591
589
|
);
|
|
592
590
|
values.media = mediaPayload;
|
|
593
591
|
|
|
@@ -15,7 +15,7 @@ async function processMessage(message, messageType) {
|
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const values = getMessageValues(message, content
|
|
18
|
+
const values = getMessageValues(message, content);
|
|
19
19
|
await insertMessage(values);
|
|
20
20
|
|
|
21
21
|
return values;
|
|
@@ -28,7 +28,7 @@ async function processMessage(message, messageType) {
|
|
|
28
28
|
async function processMediaMessage(message, logger, messageType, bucketName, sock) {
|
|
29
29
|
try {
|
|
30
30
|
const { content, contentType, reply } = extractContentTypeAndReply(message, messageType);
|
|
31
|
-
let values = getMessageValues(message, content
|
|
31
|
+
let values = getMessageValues(message, content);
|
|
32
32
|
// Insert message into the message database
|
|
33
33
|
|
|
34
34
|
const messageID = message.key.id;
|
|
@@ -108,7 +108,7 @@ function extractContentTypeAndReply(message, messageType) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
async function getLastMessages(chatId, n) {
|
|
111
|
-
const messages = await Message.find({
|
|
111
|
+
const messages = await Message.find({ numero: chatId })
|
|
112
112
|
.sort({ createdAt: -1 })
|
|
113
113
|
.limit(n)
|
|
114
114
|
.select('timestamp numero nombre_whatsapp body');
|
|
@@ -213,7 +213,7 @@ const processMediaFilesCore = async (code, reply, provider) => {
|
|
|
213
213
|
url_generation_ms: 0
|
|
214
214
|
};
|
|
215
215
|
|
|
216
|
-
if (!reply.
|
|
216
|
+
if (!reply.media) {
|
|
217
217
|
return { messagesChat, url, tempFiles, timings };
|
|
218
218
|
}
|
|
219
219
|
|
|
@@ -282,7 +282,7 @@ const processMediaFiles = withTracing(
|
|
|
282
282
|
'process_media_files',
|
|
283
283
|
(code, reply) => ({
|
|
284
284
|
'media.message_id': reply.message_id,
|
|
285
|
-
'media.is_media': reply.
|
|
285
|
+
'media.is_media': !!reply.media
|
|
286
286
|
})
|
|
287
287
|
);
|
|
288
288
|
|
|
@@ -325,7 +325,7 @@ const processThreadMessageCore = async (code, replies, provider) => {
|
|
|
325
325
|
index: i + 1,
|
|
326
326
|
total: replyArray.length,
|
|
327
327
|
isPatient,
|
|
328
|
-
hasMedia: reply.
|
|
328
|
+
hasMedia: !!reply.media,
|
|
329
329
|
hasUrl: !!url
|
|
330
330
|
});
|
|
331
331
|
|
|
@@ -104,13 +104,14 @@ async function downloadMedia(twilioMessage, logger) {
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
const ensureWhatsAppFormat = (phoneNumber) => {
|
|
107
|
-
if (!phoneNumber) return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return `whatsapp
|
|
107
|
+
if (!phoneNumber || typeof phoneNumber !== 'string') return phoneNumber;
|
|
108
|
+
|
|
109
|
+
const trimmed = phoneNumber.trim();
|
|
110
|
+
if (trimmed.startsWith('whatsapp:')) return trimmed;
|
|
111
|
+
if (trimmed.includes('@')) return trimmed;
|
|
112
|
+
if (trimmed.startsWith('+')) return `whatsapp:${trimmed}`;
|
|
113
|
+
if (/^\d+$/.test(trimmed)) return `whatsapp:+${trimmed}`;
|
|
114
|
+
return trimmed;
|
|
114
115
|
};
|
|
115
116
|
|
|
116
117
|
|
package/lib/index.d.ts
CHANGED
|
@@ -286,7 +286,7 @@ declare module '@peopl-health/nexus' {
|
|
|
286
286
|
export function delay(ms: number): Promise<void>;
|
|
287
287
|
export function formatCode(codeBase: string): string;
|
|
288
288
|
export function calculateDelay(sendTime: string | Date, timeZone?: string): number;
|
|
289
|
-
export function ensureWhatsAppFormat(phoneNumber:
|
|
289
|
+
export function ensureWhatsAppFormat(phoneNumber: any): string | null;
|
|
290
290
|
export function convertTwilioToInternalFormat(twilioMessage: any): any;
|
|
291
291
|
export function downloadMediaFromTwilio(mediaUrl: string, credentials: any): Promise<Buffer>;
|
|
292
292
|
export function getMediaTypeFromContentType(contentType: string): string;
|
|
@@ -296,7 +296,7 @@ declare module '@peopl-health/nexus' {
|
|
|
296
296
|
// Models
|
|
297
297
|
export const Message: mongoose.Model<any>;
|
|
298
298
|
export const Thread: mongoose.Model<any>;
|
|
299
|
-
export function getMessageValues(message: any, content: string
|
|
299
|
+
export function getMessageValues(message: any, content: string): any;
|
|
300
300
|
export function formatTimestamp(unixTimestamp: number): string;
|
|
301
301
|
|
|
302
302
|
// Module Exports
|
|
@@ -6,15 +6,12 @@ const { logger } = require('../utils/logger');
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
const messageSchema = new mongoose.Schema({
|
|
9
|
-
raw: { type: Object,
|
|
10
|
-
nombre_whatsapp: { type: String, required: true },
|
|
11
|
-
numero: { type: String, required: true },
|
|
9
|
+
raw: { type: Object, default: null },
|
|
12
10
|
body: { type: String, required: true },
|
|
11
|
+
numero: { type: String, required: true },
|
|
12
|
+
nombre_whatsapp: { type: String, default: null },
|
|
13
13
|
timestamp: { type: String, required: true, default: Date.now },
|
|
14
|
-
message_id: { type: String,
|
|
15
|
-
is_group: { type: Boolean, required: true },
|
|
16
|
-
is_media: { type: Boolean, required: true },
|
|
17
|
-
is_interactive: { type: Boolean, default: false },
|
|
14
|
+
message_id: { type: String, default: null},
|
|
18
15
|
interactive_type: {
|
|
19
16
|
type: String,
|
|
20
17
|
enum: ['button', 'list', 'flow', 'quick_reply'],
|
|
@@ -22,30 +19,37 @@ const messageSchema = new mongoose.Schema({
|
|
|
22
19
|
},
|
|
23
20
|
interactive_data: { type: Object, default: null },
|
|
24
21
|
group_id: { type: String, default: null },
|
|
25
|
-
reply_id: { type: String, default: null },
|
|
26
22
|
processed: { type: Boolean, default: false },
|
|
27
|
-
|
|
23
|
+
read: { type: Boolean, default: false },
|
|
28
24
|
assistant_id: { type: String, default: null },
|
|
29
25
|
content_sid: { type: String, default: null },
|
|
30
26
|
from_me: { type: Boolean, default: false },
|
|
31
27
|
origin: {
|
|
32
28
|
type: String,
|
|
33
29
|
enum: ['whatsapp_platform', 'assistant', 'patient', 'system', 'instruction'],
|
|
34
|
-
default: 'whatsapp_platform'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
default: 'whatsapp_platform'
|
|
31
|
+
},
|
|
32
|
+
tools_executed: {
|
|
33
|
+
type: [{
|
|
34
|
+
tool_name: { type: String, required: true },
|
|
35
|
+
tool_arguments: { type: Object, default: null },
|
|
36
|
+
tool_output: { type: Object, default: null },
|
|
37
|
+
execution_time_ms: { type: Number, default: null },
|
|
38
|
+
success: { type: Boolean, default: true },
|
|
39
|
+
call_id: { type: String, default: null },
|
|
40
|
+
executed_at: { type: Date, default: Date.now }
|
|
41
|
+
}],
|
|
42
|
+
default: []
|
|
43
|
+
},
|
|
44
44
|
media: {
|
|
45
45
|
contentType: { type: String, default: null },
|
|
46
46
|
bucketName: { type: String, default: null },
|
|
47
47
|
key: { type: String, default: null },
|
|
48
|
-
mediaType: {
|
|
48
|
+
mediaType: {
|
|
49
|
+
type: String,
|
|
50
|
+
enum: ['image', 'video', 'audio', 'document', 'sticker', 'other'],
|
|
51
|
+
default: null
|
|
52
|
+
},
|
|
49
53
|
fileName: { type: String, default: null },
|
|
50
54
|
fileSize: { type: Number, default: null },
|
|
51
55
|
duration: { type: Number, default: null },
|
|
@@ -60,34 +64,19 @@ const messageSchema = new mongoose.Schema({
|
|
|
60
64
|
default: 'active',
|
|
61
65
|
index: true
|
|
62
66
|
},
|
|
63
|
-
read: {
|
|
64
|
-
type: Boolean,
|
|
65
|
-
default: false
|
|
66
|
-
},
|
|
67
67
|
statusInfo: {
|
|
68
68
|
status: {
|
|
69
69
|
type: String,
|
|
70
70
|
enum: ['queued', 'sending', 'sent', 'delivered', 'undelivered', 'failed', 'read', null],
|
|
71
71
|
default: null
|
|
72
72
|
},
|
|
73
|
-
errorCode: {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
errorMessage: {
|
|
78
|
-
type: String,
|
|
79
|
-
default: null
|
|
80
|
-
},
|
|
81
|
-
updatedAt: {
|
|
82
|
-
type: Date,
|
|
83
|
-
default: null
|
|
84
|
-
}
|
|
73
|
+
errorCode: { type: String, default: null },
|
|
74
|
+
errorMessage: { type: String, default: null },
|
|
75
|
+
updatedAt: { type: Date, default: null }
|
|
85
76
|
}
|
|
86
77
|
}, { timestamps: true });
|
|
87
78
|
|
|
88
|
-
messageSchema.index({ message_id: 1, timestamp: 1 }, { unique: true });
|
|
89
79
|
messageSchema.index({ numero: 1, createdAt: -1 });
|
|
90
|
-
|
|
91
80
|
messageSchema.index({ numero: 1, processed: 1, origin: 1 }, { name: 'numero_processed_origin_idx' });
|
|
92
81
|
messageSchema.index({ numero: 1, createdAt: -1, processed: 1 }, { name: 'numero_created_processed_idx' });
|
|
93
82
|
|
|
@@ -115,47 +104,30 @@ async function getClinicalContext(whatsappId) {
|
|
|
115
104
|
|
|
116
105
|
async function insertMessage(values) {
|
|
117
106
|
try {
|
|
118
|
-
const skipNumbers = ['5215592261426@s.whatsapp.net', '5215547411345@s.whatsapp.net', '51985959446@s.whatsapp.net'];
|
|
119
107
|
const clinical_context = await getClinicalContext(values.numero);
|
|
120
|
-
const messageData =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
is_media: values.is_media,
|
|
128
|
-
is_interactive: values.is_interactive || false,
|
|
129
|
-
interactive_type: values.interactive_type || null,
|
|
130
|
-
interactive_data: values.interactive_data || null,
|
|
131
|
-
group_id: values.group_id,
|
|
132
|
-
reply_id: values.reply_id,
|
|
133
|
-
from_me: values.from_me,
|
|
134
|
-
processed: values.processed || skipNumbers.includes(values.numero),
|
|
135
|
-
media: values.media ? values.media : null,
|
|
136
|
-
content_sid: values.content_sid || null,
|
|
137
|
-
clinical_context: clinical_context,
|
|
138
|
-
origin: values.origin,
|
|
139
|
-
tools_executed: values.tools_executed || [],
|
|
140
|
-
raw: values.raw || null,
|
|
141
|
-
assistant_id: values.assistant_id || null,
|
|
142
|
-
statusInfo: values.statusInfo || (values.delivery_status ? {
|
|
108
|
+
const messageData = Object.fromEntries(
|
|
109
|
+
Object.entries(values)
|
|
110
|
+
.filter(([k, v]) => v !== undefined && !k.startsWith('delivery_'))
|
|
111
|
+
);
|
|
112
|
+
messageData.clinical_context = clinical_context;
|
|
113
|
+
if (!messageData.statusInfo && values.delivery_status) {
|
|
114
|
+
messageData.statusInfo = {
|
|
143
115
|
status: values.delivery_status,
|
|
144
116
|
errorCode: values.delivery_error_code || null,
|
|
145
117
|
errorMessage: values.delivery_error_message || null,
|
|
146
118
|
updatedAt: values.delivery_status_updated_at || null
|
|
147
|
-
}
|
|
148
|
-
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
149
121
|
|
|
150
122
|
await Message.findOneAndUpdate(
|
|
151
123
|
{ message_id: values.message_id, body: values.body },
|
|
152
124
|
{ $setOnInsert: messageData },
|
|
153
|
-
{ upsert: true, new: true }
|
|
125
|
+
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
|
154
126
|
);
|
|
155
127
|
|
|
156
128
|
logger.info('[MongoStorage] Message inserted or updated successfully');
|
|
157
129
|
} catch (err) {
|
|
158
|
-
logger.error('[MongoStorage] Error inserting message
|
|
130
|
+
logger.error('[MongoStorage] Error inserting message', { error: err.message, stack: err.stack });
|
|
159
131
|
throw err;
|
|
160
132
|
}
|
|
161
133
|
}
|
|
@@ -170,15 +142,13 @@ function formatTimestamp(unixTimestamp) {
|
|
|
170
142
|
}
|
|
171
143
|
|
|
172
144
|
|
|
173
|
-
function getMessageValues(message, content
|
|
145
|
+
function getMessageValues(message, content) {
|
|
174
146
|
const nombre_whatsapp = message.pushName;
|
|
175
147
|
const numero = message.key.participant || message.key.remoteJid;
|
|
176
148
|
const body = content;
|
|
177
149
|
const timestamp = formatTimestamp(message.messageTimestamp);
|
|
178
150
|
const message_id = message.key.id;
|
|
179
|
-
const
|
|
180
|
-
const group_id = is_group ? message.key.remoteJid : null;
|
|
181
|
-
const reply_id = reply || null;
|
|
151
|
+
const group_id = message.key.remoteJid || null;
|
|
182
152
|
const from_me = message.key.fromMe;
|
|
183
153
|
|
|
184
154
|
return {
|
|
@@ -187,10 +157,7 @@ function getMessageValues(message, content, reply, is_media) {
|
|
|
187
157
|
body,
|
|
188
158
|
timestamp,
|
|
189
159
|
message_id,
|
|
190
|
-
is_group,
|
|
191
|
-
is_media,
|
|
192
160
|
group_id,
|
|
193
|
-
reply_id,
|
|
194
161
|
from_me
|
|
195
162
|
};
|
|
196
163
|
}
|
|
@@ -98,8 +98,6 @@ const addMsgAssistantCore = async (code, inMessages, role = 'user', reply = fals
|
|
|
98
98
|
numero: code,
|
|
99
99
|
body: message,
|
|
100
100
|
message_id: message_id,
|
|
101
|
-
is_group: false,
|
|
102
|
-
is_media: false,
|
|
103
101
|
from_me: true,
|
|
104
102
|
processed: true,
|
|
105
103
|
origin: 'system',
|
|
@@ -144,8 +142,6 @@ const addInstructionCore = async (code, instruction, role = 'user') => {
|
|
|
144
142
|
numero: code,
|
|
145
143
|
body: instruction,
|
|
146
144
|
message_id: message_id,
|
|
147
|
-
is_group: false,
|
|
148
|
-
is_media: false,
|
|
149
145
|
from_me: true,
|
|
150
146
|
processed: true,
|
|
151
147
|
origin: 'instruction',
|
|
@@ -5,270 +5,101 @@ const { logger } = require('../utils/logger');
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
const fetchConversationData = async (filter, skip, limit) => {
|
|
8
|
-
|
|
8
|
+
const baseMatch = { group_id: null };
|
|
9
|
+
const unreadMatch = { from_me: false, $or: [{ read: false }, { read: { $exists: false } }] };
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
logger.info('Applying unread filter');
|
|
21
|
-
break;
|
|
22
|
-
|
|
23
|
-
case 'no-response':
|
|
24
|
-
logger.info('Applying no-response filter');
|
|
25
|
-
break;
|
|
26
|
-
|
|
27
|
-
case 'recent': {
|
|
28
|
-
const yesterday = new Date();
|
|
29
|
-
yesterday.setDate(yesterday.getDate() - 1);
|
|
30
|
-
filterConditions = {
|
|
31
|
-
is_group: false,
|
|
32
|
-
createdAt: { $gt: yesterday }
|
|
33
|
-
};
|
|
34
|
-
logger.info('Applying recent filter (last 24 hours)');
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
case 'all':
|
|
39
|
-
default:
|
|
40
|
-
filterConditions = { is_group: false };
|
|
41
|
-
logger.info('Applying all conversations filter');
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
logger.info('Executing aggregation pipeline...');
|
|
46
|
-
const aggregationStartTime = Date.now();
|
|
11
|
+
const filterMap = {
|
|
12
|
+
unread: { ...baseMatch, ...unreadMatch },
|
|
13
|
+
recent: { ...baseMatch, createdAt: { $gt: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
|
|
14
|
+
'no-response': baseMatch,
|
|
15
|
+
all: baseMatch
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const filterConditions = filterMap[filter] || baseMatch;
|
|
19
|
+
logger.info(`Applying ${filter} filter`);
|
|
47
20
|
|
|
48
|
-
|
|
21
|
+
const pipeline = [
|
|
49
22
|
{ $match: filterConditions },
|
|
50
|
-
{ $project: {
|
|
51
|
-
numero: 1,
|
|
52
|
-
body: 1,
|
|
53
|
-
createdAt: 1,
|
|
54
|
-
timestamp: 1,
|
|
55
|
-
is_media: 1,
|
|
56
|
-
media: 1,
|
|
57
|
-
nombre_whatsapp: 1,
|
|
58
|
-
from_me: 1
|
|
59
|
-
}},
|
|
23
|
+
{ $project: { numero: 1, body: 1, createdAt: 1, timestamp: 1, media: 1, nombre_whatsapp: 1, from_me: 1 } },
|
|
60
24
|
{ $sort: { createdAt: 1, timestamp: 1 } },
|
|
61
|
-
{ $group: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
messageCount: { $sum: 1 }
|
|
65
|
-
}},
|
|
66
|
-
{ $sort: { 'latestMessage.createdAt': -1 } }
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
if (filter === 'no-response') {
|
|
70
|
-
aggregationPipeline.splice(-1, 0, { $match: { 'latestMessage.from_me': false } });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
aggregationPipeline.push(
|
|
25
|
+
{ $group: { _id: '$numero', latestMessage: { $last: '$$ROOT' }, messageCount: { $sum: 1 } } },
|
|
26
|
+
...(filter === 'no-response' ? [{ $match: { 'latestMessage.from_me': false } }] : []),
|
|
27
|
+
{ $sort: { 'latestMessage.createdAt': -1 } },
|
|
74
28
|
{ $skip: skip },
|
|
75
29
|
{ $limit: limit }
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const conversations = await Message.aggregate(aggregationPipeline);
|
|
79
|
-
|
|
80
|
-
const aggregationTime = Date.now() - aggregationStartTime;
|
|
81
|
-
logger.info(`Aggregation completed in ${aggregationTime}ms, found ${conversations.length} conversations`);
|
|
82
|
-
|
|
83
|
-
// Fetch names from Airtable and WhatsApp
|
|
84
|
-
const phoneNumbers = conversations.map(conv => conv._id).filter(Boolean);
|
|
85
|
-
const formula = 'OR(' +
|
|
86
|
-
phoneNumbers.map(p => `{whatsapp_id} = "${p}"`).join(', ') +
|
|
87
|
-
')';
|
|
88
|
-
const patientTable = await getRecordByFilter(Historial_Clinico_ID, 'estado_general', formula);
|
|
89
|
-
const airtableNameMap = patientTable.reduce((map, patient) => {
|
|
90
|
-
map[patient.whatsapp_id] = patient.name;
|
|
91
|
-
return map;
|
|
92
|
-
}, {});
|
|
93
|
-
logger.info(`Found ${Object.keys(airtableNameMap).length} names in Airtable`);
|
|
94
|
-
|
|
95
|
-
const contactNames = await Message.aggregate([
|
|
96
|
-
{ $match: { is_group: false, from_me: false } },
|
|
97
|
-
{ $sort: { createdAt: -1 } },
|
|
98
|
-
{ $group: {
|
|
99
|
-
_id: '$numero',
|
|
100
|
-
name: { $first: '$nombre_whatsapp' }
|
|
101
|
-
}}
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
const nameMap = contactNames?.reduce((map, contact) => {
|
|
105
|
-
if (contact && contact._id) {
|
|
106
|
-
if (!airtableNameMap[contact._id]) {
|
|
107
|
-
map[contact._id] = contact.name || 'Unknown';
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return map;
|
|
111
|
-
}, {...airtableNameMap}) || airtableNameMap || {};
|
|
112
|
-
|
|
113
|
-
// Fetch unread counts
|
|
114
|
-
logger.info('Fetching unread counts using Message.aggregate');
|
|
115
|
-
const unreadCounts = await Message.aggregate([
|
|
116
|
-
{
|
|
117
|
-
$match: {
|
|
118
|
-
is_group: false,
|
|
119
|
-
from_me: false,
|
|
120
|
-
$or: [
|
|
121
|
-
{ read: false },
|
|
122
|
-
{ read: { $exists: false } }
|
|
123
|
-
]
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
{ $group: {
|
|
127
|
-
_id: '$numero',
|
|
128
|
-
unreadCount: { $sum: 1 }
|
|
129
|
-
}}
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
|
-
const unreadMap = unreadCounts?.reduce((map, item) => {
|
|
133
|
-
if (item && item._id) {
|
|
134
|
-
map[item._id] = item.unreadCount || 0;
|
|
135
|
-
}
|
|
136
|
-
return map;
|
|
137
|
-
}, {}) || {};
|
|
138
|
-
logger.info('Unread map calculated', { unreadMap });
|
|
139
|
-
logger.info('Conversations found', { count: conversations?.length || 0 });
|
|
140
|
-
|
|
141
|
-
// Calculate total count for pagination
|
|
142
|
-
let totalFilterConditions = { is_group: false };
|
|
143
|
-
|
|
144
|
-
if (filter === 'unread') {
|
|
145
|
-
totalFilterConditions = {
|
|
146
|
-
is_group: false,
|
|
147
|
-
from_me: false,
|
|
148
|
-
$or: [
|
|
149
|
-
{ read: false },
|
|
150
|
-
{ read: { $exists: false } }
|
|
151
|
-
]
|
|
152
|
-
};
|
|
153
|
-
} else if (filter === 'recent') {
|
|
154
|
-
const yesterday = new Date();
|
|
155
|
-
yesterday.setDate(yesterday.getDate() - 1);
|
|
156
|
-
totalFilterConditions = {
|
|
157
|
-
is_group: false,
|
|
158
|
-
createdAt: { $gt: yesterday }
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let totalAggregationPipeline = [
|
|
163
|
-
{ $match: totalFilterConditions },
|
|
164
|
-
{ $group: { _id: '$numero' } },
|
|
165
|
-
{ $count: 'total' }
|
|
166
30
|
];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return { conversations, total, nameMap, unreadMap };
|
|
31
|
+
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
const [conversations, contactNames, unreadCounts, totalResult] = await Promise.all([
|
|
34
|
+
Message.aggregate(pipeline),
|
|
35
|
+
Message.aggregate([{ $match: { ...baseMatch, from_me: false } }, { $sort: { createdAt: -1 } }, { $group: { _id: '$numero', name: { $first: '$nombre_whatsapp' } } }]),
|
|
36
|
+
Message.aggregate([{ $match: { ...baseMatch, ...unreadMatch } }, { $group: { _id: '$numero', unreadCount: { $sum: 1 } } }]),
|
|
37
|
+
Message.aggregate(filter === 'no-response'
|
|
38
|
+
? [{ $match: baseMatch }, { $project: { numero: 1, from_me: 1, createdAt: 1, timestamp: 1 } }, { $sort: { createdAt: -1 } }, { $group: { _id: '$numero', latestMessage: { $first: '$$ROOT' } } }, { $match: { 'latestMessage.from_me': false } }, { $count: 'total' }]
|
|
39
|
+
: [{ $match: filterConditions }, { $group: { _id: '$numero' } }, { $count: 'total' }]
|
|
40
|
+
)
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
logger.info(`Queries completed in ${Date.now() - startTime}ms, found ${conversations.length} conversations`);
|
|
44
|
+
|
|
45
|
+
const phoneNumbers = conversations.map(conv => conv._id).filter(Boolean);
|
|
46
|
+
const airtableNameMap = phoneNumbers.length ?
|
|
47
|
+
(await getRecordByFilter(Historial_Clinico_ID, 'estado_general', `OR(${phoneNumbers.map(p => `{whatsapp_id} = "${p}"`).join(', ')})`)).reduce((map, patient) => ({ ...map, [patient.whatsapp_id]: patient.name }), {}) : {};
|
|
48
|
+
|
|
49
|
+
const nameMap = { ...airtableNameMap, ...contactNames.reduce((map, contact) => contact?._id && !airtableNameMap[contact._id] ? { ...map, [contact._id]: contact.name || 'Unknown' } : map, {}) };
|
|
50
|
+
const unreadMap = unreadCounts.reduce((map, item) => item?._id ? { ...map, [item._id]: item.unreadCount || 0 } : map, {});
|
|
51
|
+
|
|
52
|
+
return { conversations, total: totalResult[0]?.total || 0, nameMap, unreadMap };
|
|
191
53
|
};
|
|
192
54
|
|
|
193
55
|
/**
|
|
194
56
|
* Processes conversations to prepare them for the response
|
|
195
57
|
*/
|
|
196
58
|
const processConversations = async (conversations, nameMap, unreadMap) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
59
|
+
const getMediaType = (media) => {
|
|
60
|
+
if (!media) return null;
|
|
61
|
+
if (media.mediaType) return media.mediaType;
|
|
62
|
+
if (!media.contentType) return null;
|
|
63
|
+
const [type, subtype] = media.contentType.split('/');
|
|
64
|
+
return type === 'application' ? 'document' : subtype === 'webp' ? 'sticker' : type;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const createConversation = (conv, index, fallback = {}) => {
|
|
68
|
+
const msg = conv?.latestMessage;
|
|
69
|
+
const phoneNumber = conv?._id || fallback.phoneNumber || `error_${index}`;
|
|
70
|
+
return {
|
|
71
|
+
phoneNumber,
|
|
72
|
+
name: nameMap[phoneNumber] || msg?.nombre_whatsapp || fallback.name || 'Unknown',
|
|
73
|
+
lastMessage: msg?.body || fallback.lastMessage || '',
|
|
74
|
+
lastMessageTime: msg?.createdAt || msg?.timestamp || new Date(),
|
|
75
|
+
messageCount: conv?.messageCount || 0,
|
|
76
|
+
unreadCount: unreadMap[phoneNumber] || 0,
|
|
77
|
+
isLastMessageMedia: !!msg?.media,
|
|
78
|
+
lastMessageType: getMediaType(msg?.media),
|
|
79
|
+
lastMessageFromMe: msg?.from_me || false
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
200
83
|
try {
|
|
201
|
-
processedConversations = (conversations || []).map((conv, index) => {
|
|
84
|
+
const processedConversations = (conversations || []).map((conv, index) => {
|
|
202
85
|
try {
|
|
203
|
-
if (!conv
|
|
204
|
-
logger.warn(`Conversation ${index} missing latestMessage:`, conv?._id
|
|
205
|
-
return {
|
|
206
|
-
phoneNumber: conv?._id || 'unknown',
|
|
207
|
-
name: 'Unknown',
|
|
208
|
-
lastMessage: '',
|
|
209
|
-
lastMessageTime: new Date(),
|
|
210
|
-
messageCount: 0,
|
|
211
|
-
unreadCount: 0,
|
|
212
|
-
isLastMessageMedia: false,
|
|
213
|
-
lastMessageType: null,
|
|
214
|
-
lastMessageFromMe: false
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const isMedia = conv.latestMessage.is_media === true;
|
|
219
|
-
let mediaType = null;
|
|
220
|
-
|
|
221
|
-
if (isMedia && conv?.latestMessage?.media) {
|
|
222
|
-
if (conv.latestMessage.media.mediaType) {
|
|
223
|
-
mediaType = conv.latestMessage.media.mediaType;
|
|
224
|
-
} else if (conv.latestMessage.media.contentType) {
|
|
225
|
-
const contentType = conv.latestMessage.media.contentType;
|
|
226
|
-
const contentTypeParts = contentType?.split('/') || ['unknown'];
|
|
227
|
-
mediaType = contentTypeParts[0] || 'unknown';
|
|
228
|
-
|
|
229
|
-
if (mediaType === 'application') {
|
|
230
|
-
mediaType = 'document';
|
|
231
|
-
} else if (contentTypeParts[1] === 'webp') {
|
|
232
|
-
mediaType = 'sticker';
|
|
233
|
-
}
|
|
234
|
-
}
|
|
86
|
+
if (!conv?.latestMessage) {
|
|
87
|
+
logger.warn(`Conversation ${index} missing latestMessage:`, conv?._id);
|
|
88
|
+
return createConversation(conv, index, { name: 'Unknown' });
|
|
235
89
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
lastMessage: conv?.latestMessage?.body || '',
|
|
241
|
-
lastMessageTime: conv?.latestMessage?.createdAt || conv?.latestMessage?.timestamp || new Date(),
|
|
242
|
-
messageCount: conv.messageCount || 0,
|
|
243
|
-
unreadCount: unreadMap[conv._id] || 0,
|
|
244
|
-
isLastMessageMedia: isMedia || false,
|
|
245
|
-
lastMessageType: mediaType || null,
|
|
246
|
-
lastMessageFromMe: conv?.latestMessage?.from_me || false
|
|
247
|
-
};
|
|
248
|
-
} catch (convError) {
|
|
249
|
-
logger.error(`Error processing conversation ${index}:`, convError);
|
|
250
|
-
return {
|
|
251
|
-
phoneNumber: conv?._id || `error_${index}`,
|
|
252
|
-
name: 'Error Processing',
|
|
253
|
-
lastMessage: 'Error processing conversation',
|
|
254
|
-
lastMessageTime: new Date(),
|
|
255
|
-
messageCount: 0,
|
|
256
|
-
unreadCount: 0,
|
|
257
|
-
isLastMessageMedia: false,
|
|
258
|
-
lastMessageType: null,
|
|
259
|
-
lastMessageFromMe: false
|
|
260
|
-
};
|
|
90
|
+
return createConversation(conv, index);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger.error(`Error processing conversation ${index}:`, error);
|
|
93
|
+
return createConversation(conv, index, { name: 'Error Processing', lastMessage: 'Error processing conversation' });
|
|
261
94
|
}
|
|
262
95
|
});
|
|
263
|
-
|
|
264
|
-
logger.info(`
|
|
265
|
-
|
|
266
|
-
} catch (
|
|
267
|
-
logger.error('Error in conversation mapping:',
|
|
268
|
-
|
|
96
|
+
|
|
97
|
+
logger.info(`Processed ${processedConversations.length} conversations`);
|
|
98
|
+
return processedConversations;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.error('Error in conversation mapping:', error);
|
|
101
|
+
return [];
|
|
269
102
|
}
|
|
270
|
-
|
|
271
|
-
return processedConversations;
|
|
272
103
|
};
|
|
273
104
|
|
|
274
105
|
module.exports = {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const mongoose = require('mongoose');
|
|
2
2
|
const runtimeConfig = require('../config/runtimeConfig');
|
|
3
|
+
|
|
4
|
+
const { Message, insertMessage } = require('../models/messageModel');
|
|
5
|
+
const { Interaction } = require('../models/interactionModel');
|
|
6
|
+
const { Thread } = require('../models/threadModel');
|
|
7
|
+
|
|
8
|
+
const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
|
|
9
|
+
const { processTwilioMediaMessage } = require('../helpers/twilioMediaProcessor');
|
|
3
10
|
const { logger } = require('../utils/logger');
|
|
4
11
|
|
|
5
12
|
/**
|
|
@@ -18,10 +25,6 @@ class MongoStorage {
|
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
createSchemas() {
|
|
21
|
-
const { Message } = require('../models/messageModel');
|
|
22
|
-
const { Interaction } = require('../models/interactionModel');
|
|
23
|
-
const { Thread } = require('../models/threadModel');
|
|
24
|
-
|
|
25
28
|
return {
|
|
26
29
|
Message,
|
|
27
30
|
Interaction,
|
|
@@ -45,7 +48,6 @@ class MongoStorage {
|
|
|
45
48
|
try {
|
|
46
49
|
const enrichedMessage = await this._enrichTwilioMedia(messageData);
|
|
47
50
|
const values = this.buildMessageValues(enrichedMessage);
|
|
48
|
-
const { insertMessage } = require('../models/messageModel');
|
|
49
51
|
await insertMessage(values);
|
|
50
52
|
logger.info('[MongoStorage] Message stored');
|
|
51
53
|
return values;
|
|
@@ -76,8 +78,6 @@ class MongoStorage {
|
|
|
76
78
|
return messageData;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
const { processTwilioMediaMessage } = require('../helpers/twilioMediaProcessor');
|
|
80
|
-
|
|
81
81
|
const mediaItems = await processTwilioMediaMessage(rawMessage, bucketName);
|
|
82
82
|
if (!mediaItems || mediaItems.length === 0) {
|
|
83
83
|
logger.warn('[MongoStorage] Media processing returned no items');
|
|
@@ -112,52 +112,19 @@ class MongoStorage {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
normalizeNumero(numero) {
|
|
116
|
-
if (!numero || typeof numero !== 'string') return numero;
|
|
117
|
-
|
|
118
|
-
const trimmed = numero.trim();
|
|
119
|
-
if (trimmed.startsWith('whatsapp:')) {
|
|
120
|
-
return trimmed;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (trimmed.includes('@')) {
|
|
124
|
-
return trimmed;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (trimmed.startsWith('+')) {
|
|
128
|
-
return `whatsapp:${trimmed}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (/^\d+$/.test(trimmed)) {
|
|
132
|
-
return `whatsapp:+${trimmed}`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return trimmed;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
115
|
buildMessageValues(messageData = {}) {
|
|
140
116
|
const numero = messageData.to || messageData.code || messageData.numero || messageData.from;
|
|
141
|
-
const
|
|
142
|
-
const normalizedNumero = this.normalizeNumero(rawNumero);
|
|
143
|
-
const isGroup = normalizedNumero.includes('@g.us');
|
|
117
|
+
const normalizedNumero = ensureWhatsAppFormat(numero || '');
|
|
144
118
|
const isMedia = messageData.isMedia === true || (messageData.fileType && messageData.fileType !== 'text');
|
|
145
119
|
const now = new Date();
|
|
146
|
-
const timestamp = now.toISOString();
|
|
147
|
-
const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName || runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
|
|
148
|
-
const processed = messageData.processed || false;
|
|
149
|
-
const origin = messageData.origin || 'whatsapp_platform';
|
|
150
120
|
|
|
151
|
-
|
|
152
|
-
|
|
121
|
+
const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName ||
|
|
122
|
+
runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
|
|
153
123
|
|
|
154
|
-
|
|
155
|
-
textBody = `[Media:${messageData.fileType || 'attachment'}]`;
|
|
156
|
-
} else if (!textBody) {
|
|
157
|
-
textBody = '';
|
|
158
|
-
}
|
|
124
|
+
const textBody = messageData.body || (isMedia ? `[Media:${messageData.fileType || 'attachment'}]` : '');
|
|
159
125
|
|
|
160
|
-
const providerId = messageData.messageId || messageData.sid || messageData.id || messageData._id ||
|
|
126
|
+
const providerId = messageData.messageId || messageData.sid || messageData.id || messageData._id ||
|
|
127
|
+
`pending-${now.getTime()}-${Math.floor(Math.random()*1000)}`;
|
|
161
128
|
|
|
162
129
|
const media = messageData.media || (messageData.fileUrl ? {
|
|
163
130
|
url: messageData.fileUrl,
|
|
@@ -167,33 +134,30 @@ class MongoStorage {
|
|
|
167
134
|
metadata: messageData.mediaMetadata || null
|
|
168
135
|
} : null);
|
|
169
136
|
|
|
137
|
+
const statusInfo = messageData.statusInfo || (messageData.delivery_status ? {
|
|
138
|
+
status: messageData.delivery_status,
|
|
139
|
+
errorCode: messageData.delivery_error_code || null,
|
|
140
|
+
errorMessage: messageData.delivery_error_message || null,
|
|
141
|
+
updatedAt: messageData.delivery_status_updated_at || null
|
|
142
|
+
} : null);
|
|
143
|
+
|
|
170
144
|
return {
|
|
171
145
|
nombre_whatsapp: nombre,
|
|
172
146
|
numero: normalizedNumero,
|
|
173
147
|
body: textBody,
|
|
174
|
-
timestamp,
|
|
175
|
-
processed,
|
|
148
|
+
timestamp: now.toISOString(),
|
|
149
|
+
processed: messageData.processed || false,
|
|
176
150
|
message_id: providerId,
|
|
177
|
-
is_group: isGroup,
|
|
178
|
-
is_media: isMedia,
|
|
179
|
-
is_interactive: messageData.isInteractive || false,
|
|
180
151
|
interactive_type: messageData.interactionType || null,
|
|
181
152
|
interactive_data: messageData.interactiveData || null,
|
|
182
|
-
group_id: isGroup ? normalizedNumero : null,
|
|
183
|
-
reply_id: messageData.reply_id || messageData.replyId || null,
|
|
184
153
|
from_me: messageData.fromMe !== undefined ? messageData.fromMe : true,
|
|
185
154
|
media,
|
|
186
155
|
content_sid: messageData.contentSid || null,
|
|
187
156
|
template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null,
|
|
188
157
|
raw: messageData.raw || null,
|
|
189
|
-
origin,
|
|
190
|
-
tools_executed: messageData.tools_executed
|
|
191
|
-
statusInfo
|
|
192
|
-
status: messageData.delivery_status,
|
|
193
|
-
errorCode: messageData.delivery_error_code || null,
|
|
194
|
-
errorMessage: messageData.delivery_error_message || null,
|
|
195
|
-
updatedAt: messageData.delivery_status_updated_at || null
|
|
196
|
-
} : null)
|
|
158
|
+
origin: messageData.origin || 'whatsapp_platform',
|
|
159
|
+
tools_executed: messageData.tools_executed,
|
|
160
|
+
statusInfo
|
|
197
161
|
};
|
|
198
162
|
}
|
|
199
163
|
|