@peopl-health/nexus 1.0.3 → 1.1.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.
- package/README.md +123 -0
- package/examples/basic-usage.js +22 -77
- package/lib/controllers/assistantController.js +168 -0
- package/lib/controllers/conversationController.js +582 -0
- package/lib/controllers/mediaController.js +105 -0
- package/lib/controllers/messageController.js +218 -0
- package/lib/controllers/templateController.js +631 -0
- package/lib/index.js +8 -6
- package/lib/routes/index.js +87 -0
- package/lib/utils/index.js +24 -11
- package/lib/utils/mongoAuthConfig.js +13 -3
- package/lib/utils/twilioHelper.js +0 -2
- package/lib/utils/whatsappHelper.js +0 -8
- package/package.json +14 -9
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
const { Message } = require('../models/messageModel');
|
|
2
|
+
const { sendMessage } = require('../services/whatsappService');
|
|
3
|
+
const { fetchConversationData, processConversations } = require('../services/conversationService');
|
|
4
|
+
|
|
5
|
+
const getConversationController = async (req, res) => {
|
|
6
|
+
const startTime = Date.now();
|
|
7
|
+
console.log('Starting getConversationController at', new Date().toISOString());
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Parse pagination parameters
|
|
11
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
12
|
+
const page = parseInt(req.query.page) || 1;
|
|
13
|
+
const skip = (page - 1) * limit;
|
|
14
|
+
const filter = req.query.filter || 'all';
|
|
15
|
+
|
|
16
|
+
console.log(`Pagination: page ${page}, limit ${limit}, skip ${skip}, filter: ${filter}`);
|
|
17
|
+
|
|
18
|
+
// Check if messages exist
|
|
19
|
+
const messageCount = await Message.countDocuments({});
|
|
20
|
+
console.log('Total message count:', messageCount);
|
|
21
|
+
|
|
22
|
+
if (messageCount === 0) {
|
|
23
|
+
console.log('No messages found in database, returning empty conversations list');
|
|
24
|
+
return res.status(200).json({
|
|
25
|
+
success: true,
|
|
26
|
+
conversations: [],
|
|
27
|
+
emptyDatabase: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fetch conversation data
|
|
32
|
+
const { conversations, total, nameMap, unreadMap } = await fetchConversationData(filter, skip, limit);
|
|
33
|
+
|
|
34
|
+
if (!conversations || conversations.length === 0) {
|
|
35
|
+
console.log('No conversations found, returning empty list');
|
|
36
|
+
return res.status(200).json({
|
|
37
|
+
success: true,
|
|
38
|
+
conversations: [],
|
|
39
|
+
noConversations: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Process conversations for response
|
|
44
|
+
const processedConversations = await processConversations(conversations, nameMap, unreadMap);
|
|
45
|
+
|
|
46
|
+
const totalPages = Math.ceil(total / limit);
|
|
47
|
+
const totalTime = Date.now() - startTime;
|
|
48
|
+
|
|
49
|
+
console.log('Number of conversations found:', conversations?.length || 0);
|
|
50
|
+
console.log(`Total controller execution time: ${totalTime}ms`);
|
|
51
|
+
console.log(`Filter: ${filter}, Pagination: ${conversations.length} of ${total} conversations (page ${page}/${totalPages})`);
|
|
52
|
+
|
|
53
|
+
res.status(200).json({
|
|
54
|
+
success: true,
|
|
55
|
+
conversations: processedConversations,
|
|
56
|
+
filter: filter,
|
|
57
|
+
pagination: {
|
|
58
|
+
page,
|
|
59
|
+
limit,
|
|
60
|
+
total,
|
|
61
|
+
totalPages,
|
|
62
|
+
hasNext: page < totalPages,
|
|
63
|
+
hasPrev: page > 1
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
console.log('Response sent successfully!');
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error fetching conversations:', error);
|
|
70
|
+
console.error('Error stack:', error.stack);
|
|
71
|
+
res.status(500).json({
|
|
72
|
+
success: false,
|
|
73
|
+
error: error.message || 'Failed to fetch conversations'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getConversationMessagesController = async (req, res) => {
|
|
79
|
+
console.log('Starting getConversationMessagesController at', new Date().toISOString());
|
|
80
|
+
try {
|
|
81
|
+
const { phoneNumber } = req.params;
|
|
82
|
+
console.log('Requested conversation for phone number:', phoneNumber);
|
|
83
|
+
|
|
84
|
+
if (!phoneNumber) {
|
|
85
|
+
return res.status(400).json({
|
|
86
|
+
success: false,
|
|
87
|
+
error: 'Phone number is required'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
92
|
+
const { before } = req.query;
|
|
93
|
+
|
|
94
|
+
const query = { numero: phoneNumber, is_group: false };
|
|
95
|
+
if (before) {
|
|
96
|
+
try {
|
|
97
|
+
query.createdAt = { $lt: new Date(before) };
|
|
98
|
+
} catch (parseError) {
|
|
99
|
+
console.warn('Invalid date format for before parameter:', before, parseError);
|
|
100
|
+
query.createdAt = { $lt: before };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log('Fetching conversation messages with query:', JSON.stringify(query));
|
|
105
|
+
console.log('Using limit:', limit);
|
|
106
|
+
|
|
107
|
+
console.log('About to execute Message.find with query:', JSON.stringify(query));
|
|
108
|
+
let messages = [];
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const countResult = await Message.countDocuments(query);
|
|
112
|
+
console.log(`Found ${countResult} messages matching query criteria`);
|
|
113
|
+
|
|
114
|
+
if (countResult > 0) {
|
|
115
|
+
messages = await Message.find(query)
|
|
116
|
+
.sort({ createdAt: -1 })
|
|
117
|
+
.limit(limit)
|
|
118
|
+
.lean();
|
|
119
|
+
|
|
120
|
+
console.log(`Retrieved ${messages.length} messages, inspecting for potential issues...`);
|
|
121
|
+
|
|
122
|
+
const problematicMessages = messages.filter(msg => {
|
|
123
|
+
if (!msg || !msg.numero || !msg.createdAt) {
|
|
124
|
+
console.warn('Found message missing required fields:', msg?._id || 'unknown');
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (msg.is_media === true) {
|
|
129
|
+
if (!msg.media || typeof msg.media !== 'object') {
|
|
130
|
+
console.warn('Found media message with invalid media data:', msg._id);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (msg.media && (typeof msg.media.data === 'function' || msg.media.data instanceof Buffer)) {
|
|
135
|
+
console.warn('Found media message with Buffer data that might cause serialization issues:', msg._id);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (problematicMessages.length > 0) {
|
|
144
|
+
console.warn(`Found ${problematicMessages.length} potentially problematic messages`);
|
|
145
|
+
console.log('First problematic message IDs:', problematicMessages.slice(0, 3).map(m => m?._id || 'unknown'));
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
console.log('No messages found for this query');
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('Database query error in message retrieval:', err);
|
|
152
|
+
messages = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`Retrieved a total of ${messages?.length || 0} messages for phone number: ${phoneNumber}`);
|
|
156
|
+
|
|
157
|
+
const sanitizedMessages = (messages || []).map(msg => {
|
|
158
|
+
try {
|
|
159
|
+
JSON.stringify(msg);
|
|
160
|
+
return msg;
|
|
161
|
+
} catch (serializationError) {
|
|
162
|
+
console.error(`Found non-serializable message with ID ${msg._id}:`, serializationError);
|
|
163
|
+
return {
|
|
164
|
+
_id: msg._id?.toString() || 'unknown',
|
|
165
|
+
numero: msg.numero || phoneNumber,
|
|
166
|
+
body: msg.body || '[Message content unavailable]',
|
|
167
|
+
createdAt: msg.createdAt || new Date(),
|
|
168
|
+
from_me: !!msg.from_me,
|
|
169
|
+
is_media: !!msg.is_media,
|
|
170
|
+
error: 'Message contained non-serializable data'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const reversedMessages = sanitizedMessages.reverse();
|
|
176
|
+
console.log(`Sending ${reversedMessages.length} sanitized messages in response`);
|
|
177
|
+
|
|
178
|
+
res.status(200).json({
|
|
179
|
+
success: true,
|
|
180
|
+
phoneNumber,
|
|
181
|
+
messages: reversedMessages
|
|
182
|
+
});
|
|
183
|
+
console.log('Successfully sent conversation messages response');
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, error);
|
|
186
|
+
res.status(500).json({
|
|
187
|
+
success: false,
|
|
188
|
+
error: error.message || 'Failed to fetch conversation'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const getConversationReplyController = async (req, res) => {
|
|
194
|
+
console.log('Starting getConversationReplyController at', new Date().toISOString());
|
|
195
|
+
try {
|
|
196
|
+
const { phoneNumber, message, mediaData, contentSid, variables } = req.body;
|
|
197
|
+
console.log('Reply request params:', {
|
|
198
|
+
phoneNumber,
|
|
199
|
+
hasMessage: !!message,
|
|
200
|
+
hasMediaData: !!mediaData,
|
|
201
|
+
hasContentSid: !!contentSid,
|
|
202
|
+
hasVariables: !!variables
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!phoneNumber || (!message && !mediaData && !contentSid)) {
|
|
206
|
+
return res.status(400).json({
|
|
207
|
+
success: false,
|
|
208
|
+
error: 'Phone number and either message, media data, or template ID are required'
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const formattedPhoneNumber = phoneNumber?.startsWith('whatsapp:')
|
|
213
|
+
? phoneNumber
|
|
214
|
+
: `whatsapp:${phoneNumber}`;
|
|
215
|
+
console.log('Formatted phone number:', formattedPhoneNumber);
|
|
216
|
+
|
|
217
|
+
const messageData = {
|
|
218
|
+
code: formattedPhoneNumber,
|
|
219
|
+
fileType: 'text'
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Handle template message (contentSid provided)
|
|
223
|
+
if (contentSid) {
|
|
224
|
+
console.log('Processing template message with contentSid:', contentSid);
|
|
225
|
+
messageData.contentSid = contentSid;
|
|
226
|
+
|
|
227
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
228
|
+
console.log('Template variables:', JSON.stringify(variables));
|
|
229
|
+
messageData.variables = variables;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const now = new Date();
|
|
233
|
+
const timestamp = now.getTime();
|
|
234
|
+
const uniqueId = `${Math.floor(Math.random() * 10000)}`;
|
|
235
|
+
|
|
236
|
+
messageData.templateMetadata = {
|
|
237
|
+
timestamp: timestamp,
|
|
238
|
+
uniqueId: uniqueId,
|
|
239
|
+
variableCount: variables ? Object.keys(variables).length : 0
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Handle media message
|
|
243
|
+
else if (mediaData && mediaData.fileUrl) {
|
|
244
|
+
console.log('Processing media data:', { fileType: mediaData.fileType, hasFileUrl: !!mediaData.fileUrl, fileName: mediaData.fileName });
|
|
245
|
+
messageData.fileUrl = mediaData.fileUrl;
|
|
246
|
+
messageData.fileType = mediaData.fileType || 'image';
|
|
247
|
+
|
|
248
|
+
if (mediaData.fileName) {
|
|
249
|
+
messageData.fileName = mediaData.fileName;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (message && message.trim() !== '') {
|
|
253
|
+
messageData.message = message;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Handle text message
|
|
257
|
+
else if (message) {
|
|
258
|
+
messageData.message = message;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log('Sending message with data:', JSON.stringify(messageData));
|
|
262
|
+
await sendMessage(messageData);
|
|
263
|
+
console.log('Message sent successfully');
|
|
264
|
+
|
|
265
|
+
res.status(200).json({
|
|
266
|
+
success: true,
|
|
267
|
+
message: 'Reply sent successfully'
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('Error sending reply:', error);
|
|
271
|
+
console.log('Request body:', JSON.stringify(req.body || {}));
|
|
272
|
+
const errorMsg = error.message || 'Failed to send reply';
|
|
273
|
+
console.error('Responding with error:', errorMsg);
|
|
274
|
+
res.status(500).json({
|
|
275
|
+
success: false,
|
|
276
|
+
error: errorMsg
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const searchConversationsController = async (req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
const { query, limit = 50 } = req.query;
|
|
284
|
+
|
|
285
|
+
if (!query || query.trim().length === 0) {
|
|
286
|
+
return res.status(400).json({
|
|
287
|
+
success: false,
|
|
288
|
+
error: 'Search query is required'
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`Searching conversations for query: "${query}"`);
|
|
293
|
+
const searchStartTime = Date.now();
|
|
294
|
+
|
|
295
|
+
const escapedQuery = query.replace(/\+/g, '\\+');
|
|
296
|
+
|
|
297
|
+
// Search through all conversations in the database
|
|
298
|
+
const conversations = await Message.aggregate([
|
|
299
|
+
{ $match: {
|
|
300
|
+
is_group: false,
|
|
301
|
+
$or: [
|
|
302
|
+
{ numero: { $regex: escapedQuery, $options: 'i' } },
|
|
303
|
+
{ nombre_whatsapp: { $regex: escapedQuery, $options: 'i' } },
|
|
304
|
+
{ body: { $regex: escapedQuery, $options: 'i' } }
|
|
305
|
+
]
|
|
306
|
+
}},
|
|
307
|
+
{ $project: {
|
|
308
|
+
numero: 1,
|
|
309
|
+
body: 1,
|
|
310
|
+
createdAt: 1,
|
|
311
|
+
timestamp: 1,
|
|
312
|
+
is_media: 1,
|
|
313
|
+
media: 1,
|
|
314
|
+
nombre_whatsapp: 1,
|
|
315
|
+
from_me: 1
|
|
316
|
+
}},
|
|
317
|
+
{ $group: {
|
|
318
|
+
_id: '$numero',
|
|
319
|
+
latestMessage: { $first: '$$ROOT' },
|
|
320
|
+
messageCount: { $sum: 1 }
|
|
321
|
+
}},
|
|
322
|
+
{ $sort: { 'latestMessage.createdAt': -1 } },
|
|
323
|
+
{ $limit: parseInt(limit) }
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
const searchTime = Date.now() - searchStartTime;
|
|
327
|
+
console.log(`Search completed in ${searchTime}ms, found ${conversations.length} conversations`);
|
|
328
|
+
|
|
329
|
+
// Process conversations for response
|
|
330
|
+
const processedConversations = conversations.map(conv => {
|
|
331
|
+
if (!conv || !conv.latestMessage) {
|
|
332
|
+
return {
|
|
333
|
+
phoneNumber: conv?._id || 'unknown',
|
|
334
|
+
name: 'Unknown',
|
|
335
|
+
lastMessage: '',
|
|
336
|
+
lastMessageTime: new Date(),
|
|
337
|
+
messageCount: 0,
|
|
338
|
+
unreadCount: 0,
|
|
339
|
+
isLastMessageMedia: false,
|
|
340
|
+
lastMessageType: null,
|
|
341
|
+
lastMessageFromMe: false
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const isMedia = conv.latestMessage.is_media === true;
|
|
346
|
+
let mediaType = null;
|
|
347
|
+
|
|
348
|
+
if (isMedia && conv?.latestMessage?.media) {
|
|
349
|
+
if (conv.latestMessage.media.mediaType) {
|
|
350
|
+
mediaType = conv.latestMessage.media.mediaType;
|
|
351
|
+
} else if (conv.latestMessage.media.contentType) {
|
|
352
|
+
const contentType = conv.latestMessage.media.contentType;
|
|
353
|
+
const contentTypeParts = contentType?.split('/') || ['unknown'];
|
|
354
|
+
mediaType = contentTypeParts[0] || 'unknown';
|
|
355
|
+
|
|
356
|
+
if (mediaType === 'application') {
|
|
357
|
+
mediaType = 'document';
|
|
358
|
+
} else if (contentTypeParts[1] === 'webp') {
|
|
359
|
+
mediaType = 'sticker';
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
phoneNumber: conv._id,
|
|
366
|
+
name: conv?.latestMessage?.nombre_whatsapp || 'Unknown',
|
|
367
|
+
lastMessage: conv?.latestMessage?.body || '',
|
|
368
|
+
lastMessageTime: conv?.latestMessage?.createdAt || conv?.latestMessage?.timestamp || new Date(),
|
|
369
|
+
messageCount: conv.messageCount || 0,
|
|
370
|
+
unreadCount: 0,
|
|
371
|
+
isLastMessageMedia: isMedia || false,
|
|
372
|
+
lastMessageType: mediaType || null,
|
|
373
|
+
lastMessageFromMe: conv?.latestMessage?.from_me || false
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
res.status(200).json({
|
|
378
|
+
success: true,
|
|
379
|
+
conversations: processedConversations,
|
|
380
|
+
searchInfo: {
|
|
381
|
+
query,
|
|
382
|
+
results: processedConversations.length,
|
|
383
|
+
searchTime
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error('Error searching conversations:', error);
|
|
389
|
+
res.status(500).json({
|
|
390
|
+
success: false,
|
|
391
|
+
error: error.message || 'Failed to search conversations'
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const getConversationsByNameController = async (req, res) => {
|
|
397
|
+
try {
|
|
398
|
+
const conversations = await Message.aggregate([
|
|
399
|
+
{ $match: { from_me: false, is_group: false } },
|
|
400
|
+
{ $sort: { timestamp: -1 } },
|
|
401
|
+
{ $group: {
|
|
402
|
+
_id: '$numero',
|
|
403
|
+
name: { $first: '$nombre_whatsapp' },
|
|
404
|
+
latestMessage: { $first: '$$ROOT' },
|
|
405
|
+
messageCount: { $sum: 1 }
|
|
406
|
+
}},
|
|
407
|
+
{ $sort: { 'latestMessage.timestamp': -1 } }
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
res.status(200).json({
|
|
411
|
+
success: true,
|
|
412
|
+
conversations: conversations.map(conv => ({
|
|
413
|
+
phoneNumber: conv._id,
|
|
414
|
+
name: conv.name,
|
|
415
|
+
lastMessage: conv.latestMessage.body,
|
|
416
|
+
lastMessageTime: conv.latestMessage.timestamp,
|
|
417
|
+
messageCount: conv.messageCount
|
|
418
|
+
}))
|
|
419
|
+
});
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error('Error fetching conversations by name:', error);
|
|
422
|
+
res.status(500).json({
|
|
423
|
+
success: false,
|
|
424
|
+
error: error.message || 'Failed to fetch conversations by name'
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const getNewMessagesController = async (req, res) => {
|
|
430
|
+
try {
|
|
431
|
+
const { phoneNumber } = req.params;
|
|
432
|
+
const { after, limit = 20 } = req.query;
|
|
433
|
+
|
|
434
|
+
if (!after) {
|
|
435
|
+
return res.status(400).json({
|
|
436
|
+
success: false,
|
|
437
|
+
error: 'The after parameter is required'
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const lastMessage = await Message.findById(after).lean();
|
|
442
|
+
|
|
443
|
+
if (!lastMessage) {
|
|
444
|
+
return res.status(404).json({
|
|
445
|
+
success: false,
|
|
446
|
+
error: 'Reference message not found'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const query = {
|
|
451
|
+
numero: phoneNumber,
|
|
452
|
+
is_group: false,
|
|
453
|
+
createdAt: { $gt: lastMessage.createdAt }
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const messages = await Message.find(query)
|
|
457
|
+
.sort({ createdAt: 1 })
|
|
458
|
+
.limit(parseInt(limit))
|
|
459
|
+
.lean();
|
|
460
|
+
|
|
461
|
+
res.status(200).json({
|
|
462
|
+
success: true,
|
|
463
|
+
phoneNumber,
|
|
464
|
+
messages: messages
|
|
465
|
+
});
|
|
466
|
+
} catch (error) {
|
|
467
|
+
console.error(`Error fetching new messages for ${req.params.phoneNumber}:`, error);
|
|
468
|
+
res.status(500).json({
|
|
469
|
+
success: false,
|
|
470
|
+
error: error.message || 'Failed to fetch new messages'
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const markMessagesAsReadController = async (req, res) => {
|
|
476
|
+
console.log('Starting markMessagesAsReadController at', new Date().toISOString());
|
|
477
|
+
try {
|
|
478
|
+
const { phoneNumber } = req.params;
|
|
479
|
+
|
|
480
|
+
if (!phoneNumber) {
|
|
481
|
+
return res.status(400).json({
|
|
482
|
+
success: false,
|
|
483
|
+
error: 'Phone number is required'
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log('Marking messages as read for phone number:', phoneNumber);
|
|
488
|
+
const result = await Message.updateMany(
|
|
489
|
+
{
|
|
490
|
+
numero: phoneNumber,
|
|
491
|
+
from_me: false,
|
|
492
|
+
$or: [
|
|
493
|
+
{ read: false },
|
|
494
|
+
{ read: { $exists: false } }
|
|
495
|
+
]
|
|
496
|
+
},
|
|
497
|
+
{ $set: { read: true } }
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
res.status(200).json({
|
|
501
|
+
success: true,
|
|
502
|
+
message: `${result.nModified || result.modifiedCount} messages marked as read`,
|
|
503
|
+
modifiedCount: result.nModified || result.modifiedCount
|
|
504
|
+
});
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.error(`Error marking messages as read for ${req.params?.phoneNumber || 'unknown'}:`, error);
|
|
507
|
+
console.error('Error stack:', error.stack);
|
|
508
|
+
res.status(500).json({
|
|
509
|
+
success: false,
|
|
510
|
+
error: error.message || 'Failed to mark messages as read'
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const sendTemplateToNewNumberController = async (req, res) => {
|
|
516
|
+
console.log('Starting sendTemplateToNewNumberController at', new Date().toISOString());
|
|
517
|
+
try {
|
|
518
|
+
const { phoneNumber, templateId, variables } = req.body;
|
|
519
|
+
|
|
520
|
+
if (!phoneNumber) {
|
|
521
|
+
return res.status(400).json({
|
|
522
|
+
success: false,
|
|
523
|
+
error: 'Phone number is required'
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!templateId) {
|
|
528
|
+
return res.status(400).json({
|
|
529
|
+
success: false,
|
|
530
|
+
error: 'Template ID is required'
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Format phone number for WhatsApp if needed
|
|
535
|
+
const formattedPhoneNumber = phoneNumber.startsWith('whatsapp:')
|
|
536
|
+
? phoneNumber
|
|
537
|
+
: `whatsapp:${phoneNumber}`;
|
|
538
|
+
|
|
539
|
+
// Log template details for debugging
|
|
540
|
+
console.log('Sending template to new number with details:', {
|
|
541
|
+
phoneNumber: formattedPhoneNumber,
|
|
542
|
+
templateId,
|
|
543
|
+
hasVariables: variables && Object.keys(variables).length > 0,
|
|
544
|
+
variableKeys: variables ? Object.keys(variables) : []
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const messageData = {
|
|
548
|
+
code: formattedPhoneNumber,
|
|
549
|
+
contentSid: templateId
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
553
|
+
messageData.variables = variables;
|
|
554
|
+
console.log('Template variables:', JSON.stringify(variables));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const message = await sendMessage(messageData);
|
|
558
|
+
|
|
559
|
+
res.status(200).json({
|
|
560
|
+
success: true,
|
|
561
|
+
message: 'Template sent successfully',
|
|
562
|
+
messageId: message.sid
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error('Error sending template to new number:', error);
|
|
566
|
+
res.status(500).json({
|
|
567
|
+
success: false,
|
|
568
|
+
error: error.message || 'Failed to send template to new number'
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
module.exports = {
|
|
574
|
+
getConversationController,
|
|
575
|
+
getConversationMessagesController,
|
|
576
|
+
getConversationReplyController,
|
|
577
|
+
getConversationsByNameController,
|
|
578
|
+
getNewMessagesController,
|
|
579
|
+
markMessagesAsReadController,
|
|
580
|
+
searchConversationsController,
|
|
581
|
+
sendTemplateToNewNumberController
|
|
582
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Optional AWS config - will be undefined if not available
|
|
2
|
+
let downloadFileFromS3, s3;
|
|
3
|
+
try {
|
|
4
|
+
downloadFileFromS3 = require('../config/awsConfig')?.downloadFileFromS3;
|
|
5
|
+
s3 = require('../config/awsConfig')?.s3;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
// AWS config not available
|
|
8
|
+
}
|
|
9
|
+
const bucketName = process.env.AWS_S3_BUCKET_NAME;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const getMediaController = async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const { key } = req.params;
|
|
15
|
+
|
|
16
|
+
console.log(`[MediaController] Received request for media file with key: ${key}`);
|
|
17
|
+
console.log(`[MediaController] Original URL path: ${req.originalUrl}`);
|
|
18
|
+
|
|
19
|
+
if (!key || key.trim() === '') {
|
|
20
|
+
console.error('[MediaController] Invalid key provided:', key);
|
|
21
|
+
return res.status(400).json({
|
|
22
|
+
success: false,
|
|
23
|
+
error: 'Invalid media key provided'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let mediaKey = key;
|
|
28
|
+
|
|
29
|
+
console.log(`[MediaController] Final S3 key to fetch: ${mediaKey}`);
|
|
30
|
+
|
|
31
|
+
const fileData = await downloadFileFromS3(bucketName, mediaKey);
|
|
32
|
+
|
|
33
|
+
if (!fileData || !fileData.Body) {
|
|
34
|
+
console.error(`[MediaController] Media not found in S3: ${key}`);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const prefix = key.split('/')[0];
|
|
38
|
+
console.log(`[MediaController] Checking S3 for objects with prefix: ${prefix}/`);
|
|
39
|
+
|
|
40
|
+
s3.listObjectsV2({
|
|
41
|
+
Bucket: bucketName,
|
|
42
|
+
Prefix: prefix + '/',
|
|
43
|
+
MaxKeys: 10
|
|
44
|
+
}).promise()
|
|
45
|
+
.then(listData => {
|
|
46
|
+
if (listData.Contents && listData.Contents.length > 0) {
|
|
47
|
+
console.log(`[MediaController] Found ${listData.Contents.length} objects with similar prefix:`);
|
|
48
|
+
listData.Contents.forEach(item => {
|
|
49
|
+
console.log(`[MediaController] - ${item.Key} (${item.Size} bytes)`);
|
|
50
|
+
if (item.Key.includes(key.split('/').pop().substring(0, 10))) {
|
|
51
|
+
console.log(`[MediaController] !!! POTENTIAL MATCH: ${item.Key}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`[MediaController] No objects found with prefix: ${prefix}/`);
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.catch(listErr => {
|
|
59
|
+
console.error(`[MediaController] Error listing objects: ${listErr.message}`);
|
|
60
|
+
});
|
|
61
|
+
} catch (listErr) {
|
|
62
|
+
console.error(`[MediaController] Error setting up bucket listing: ${listErr.message}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return res.status(404).json({
|
|
66
|
+
success: false,
|
|
67
|
+
error: 'Media not found in S3',
|
|
68
|
+
key: key,
|
|
69
|
+
bucket: bucketName,
|
|
70
|
+
timestamp: new Date().toISOString()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let contentType = fileData.ContentType || 'application/octet-stream';
|
|
75
|
+
|
|
76
|
+
if (contentType === 'application/octet-stream') {
|
|
77
|
+
const fileExtension = key.split('.').pop().toLowerCase();
|
|
78
|
+
|
|
79
|
+
if (['jpg', 'jpeg', 'png', 'webp'].includes(fileExtension)) {
|
|
80
|
+
contentType = `image/${fileExtension === 'jpg' ? 'jpeg' : fileExtension}`;
|
|
81
|
+
} else if (fileExtension === 'pdf') {
|
|
82
|
+
contentType = 'application/pdf';
|
|
83
|
+
} else if (['mp3', 'wav', 'ogg', 'm4a'].includes(fileExtension)) {
|
|
84
|
+
contentType = `audio/${fileExtension}`;
|
|
85
|
+
} else if (['mp4', 'webm', '3gp'].includes(fileExtension)) {
|
|
86
|
+
contentType = `video/${fileExtension}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
91
|
+
res.setHeader('Content-Type', contentType);
|
|
92
|
+
|
|
93
|
+
res.send(fileData.Body);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Error serving media:', error);
|
|
96
|
+
res.status(500).json({
|
|
97
|
+
success: false,
|
|
98
|
+
error: 'Failed to retrieve media'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
getMediaController
|
|
105
|
+
};
|