@peopl-health/nexus 2.2.10 → 2.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.
@@ -108,12 +108,6 @@ class TwilioProvider extends MessageProvider {
108
108
  const result = await this.twilioClient.messages.create(messageParams);
109
109
  if (this.messageStorage && typeof this.messageStorage.saveMessage === 'function') {
110
110
  try {
111
- console.log('[TwilioProvider] Persisting outbound message', {
112
- code: formattedCode,
113
- from: formattedFrom,
114
- hasMedia: Boolean(messageParams.mediaUrl && messageParams.mediaUrl.length),
115
- hasTemplate: Boolean(messageParams.contentSid)
116
- });
117
111
  await this.messageStorage.saveMessage({
118
112
  ...messageData,
119
113
  code: formattedCode,
@@ -1,18 +1,12 @@
1
- const { downloadFileFromS3, generatePresignedUrl } = require('../config/awsConfig.js');
2
1
  const llmConfig = require('../config/llmConfig.js');
3
2
 
4
- const { Message } = require('../models/messageModel.js');
5
-
6
- const { convertPdfToImages } = require('./filesHelper.js');
7
- const { analyzeImage } = require('../helpers/llmsHelper.js');
3
+ const { Thread } = require('../models/threadModel.js');
4
+ const { createProvider } = require('../providers/createProvider.js');
5
+ const { withTracing } = require('../utils/tracingDecorator');
8
6
 
9
7
  const { getRecordByFilter } = require('../services/airtableService.js');
10
8
 
11
- const fs = require('fs');
12
- const path = require('path');
13
- const moment = require('moment-timezone');
14
-
15
- const mode = process.env.NODE_ENV || 'dev';
9
+ const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
16
10
 
17
11
  async function checkIfFinished(text) {
18
12
  try {
@@ -33,292 +27,121 @@ async function checkIfFinished(text) {
33
27
 
34
28
  return completion.choices[0].message.content;
35
29
  } catch (error) {
36
- console.error('Error checking run status:', error);
30
+ console.error('[checkIfFinished] Error checking run status:', error);
37
31
  }
38
32
  }
39
33
 
40
- async function getLastMessages(code) {
41
- try {
42
- let query = { processed: false };
43
- if (code.endsWith('@g.us')) {
44
- query.group_id = code;
45
- query.numero = { $nin: ['5215592261426@s.whatsapp.net', '5215547411345@s.whatsapp.net'] };
46
- } else {
47
- query.numero = code;
48
- query.is_group = false;
49
- }
50
-
51
- const lastMessages = await Message.find(query).sort({ createdAt: -1 });
52
- console.log('[getLastMessages] lastMessages', lastMessages.map(msg => msg.body).join('\n\n'));
53
-
54
- if (lastMessages.length === 0) return [];
55
-
56
- let patientReply = [];
57
- for (const message of lastMessages) {
58
- patientReply.push(message);
59
- await Message.updateOne(
60
- { message_id: message.message_id, timestamp: message.timestamp },
61
- { $set: { processed: true } }
62
- );
63
- }
64
-
65
- console.log('[getLastMessages] Marked', patientReply.length, 'messages as processed');
66
- return patientReply.reverse();
67
- } catch (error) {
68
- console.error('Error getting the last user messages:', error);
69
- return [];
34
+ function getCurRow(baseID, code) {
35
+ if (code.endsWith('@g.us')) {
36
+ return getRecordByFilter(baseID, 'estado_general', `FIND("${code}", {Group ID})`);
37
+ } else {
38
+ return getRecordByFilter(baseID, 'estado_general', `FIND("${code.split('@')[0]}", {whatsapp_id})`);
70
39
  }
71
40
  }
72
41
 
73
- async function getLastNMessages(code, n) {
74
- try {
75
- const lastMessages = await Message.find({ numero: code })
76
- .sort({ createdAt: -1 })
77
- .limit(n);
78
-
79
- // Format each message and concatenate them with skip lines
80
- const formattedMessages = lastMessages
81
- .reverse()
82
- .map(message => formatMessage(message))
83
- .filter(formatted => formatted !== null) // Filter out any null results from formatMessage
84
- .join('\n\n');
85
-
86
- console.log('[getLastNMessages] Fetched last messages:', formattedMessages);
87
-
88
- return formattedMessages;
89
- } catch (error) {
90
- console.error('Error retrieving the last user messages:', error);
91
- return '';
42
+ const runAssistantAndWait = async ({
43
+ thread,
44
+ assistant,
45
+ runConfig = {}
46
+ }) => {
47
+ if (!thread || !thread.getConversationId()) {
48
+ throw new Error('runAssistantAndWait requires a thread with a valid thread_id or conversation_id');
92
49
  }
93
- }
94
50
 
95
- const getPatientRoleAndName = (reply, numbers) => {
96
- let role = 'familiar';
97
- let name = reply.nombre_whatsapp;
98
- for (const [key, value] of Object.entries(numbers)) {
99
- console.log(key, value, reply.numero);
100
- if (value[reply.numero]) {
101
- role = key;
102
- name = value[reply.numero];
103
- break;
104
- }
51
+ if (!assistant) {
52
+ throw new Error('runAssistantAndWait requires an assistant instance');
105
53
  }
106
- if (mode === 'prod') {
107
- if (reply.from_me) {
108
- role = 'asistente';
109
- name = 'Pipo';
110
- }
111
- }
112
- return { role, name };
113
- };
114
54
 
115
- function formatMessage(reply) {
116
- try {
117
- if (!reply.timestamp) {
118
- return null;
119
- }
120
-
121
- // Normalize timestamp: convert any format to a Date object, then to ISO string
122
- let dateObj;
123
-
124
- if (reply.timestamp instanceof Date) {
125
- dateObj = reply.timestamp;
126
- } else if (typeof reply.timestamp === 'number') {
127
- const ms = reply.timestamp < 1e12 ? reply.timestamp * 1000 : reply.timestamp;
128
- dateObj = new Date(ms);
129
- } else {
130
- dateObj = new Date(reply.timestamp);
131
- }
132
-
133
- if (isNaN(dateObj.getTime())) {
134
- console.warn('[formatMessage] Invalid timestamp:', reply.timestamp);
135
- return null;
136
- }
137
-
138
- const isoString = dateObj.toISOString();
139
-
140
- // Convert timestamp to Mexico City timezone with Spanish format
141
- // Format: martes, 30 de septiembre de 2025 a las 8:30 AM
142
- const mexicoCityTime = moment(isoString)
143
- .tz('America/Mexico_City')
144
- .locale('es')
145
- .format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
146
-
147
- return `[${mexicoCityTime}] ${reply.body}`;
148
- } catch (error) {
149
- console.error('[formatMessage] Error formatting message:', error?.message || error, 'timestamp:', reply.timestamp);
150
- return null;
151
- }
152
- }
55
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
56
+ const { polling, tools: configTools, ...conversationConfig } = runConfig || {};
57
+ const variant = provider.getVariant ? provider.getVariant() : (process.env.VARIANT || 'assistants');
58
+ const tools = assistant.getToolSchemas ? assistant.getToolSchemas() : (configTools || []);
153
59
 
154
- async function downloadMediaAndCreateFile(code, reply) {
155
- const resultMedia = await Message.findOne({
156
- message_id: reply.message_id,
157
- timestamp: reply.timestamp,
158
- media: { $ne: null }
159
- });
60
+ const runConfigWithAssistant = variant === 'responses'
61
+ ? { ...conversationConfig, assistant }
62
+ : conversationConfig;
160
63
 
161
- if (!resultMedia) return [];
64
+ let run = await provider.runConversation({
65
+ threadId: thread.getConversationId(),
66
+ assistantId: thread.getAssistantId(),
67
+ tools: tools.length > 0 ? tools : undefined,
68
+ ...runConfigWithAssistant,
69
+ });
162
70
 
163
- if (!resultMedia.media || !resultMedia.media.key) {
164
- console.log('[downloadMediaAndCreateFile] No valid media found for message:', reply.message_id);
165
- return [];
71
+ const filter = thread.code ? { code: thread.code, active: true } : null;
72
+ if (filter) {
73
+ await Thread.updateOne(filter, { $set: { run_id: run.id } });
166
74
  }
167
75
 
168
- const { bucketName, key } = resultMedia.media;
169
- if (!bucketName || !key) return [];
170
- const [subType, fileName] = key.split('/');
171
- const sourceFile = `${code}-${subType}-${fileName}`;
172
- const downloadPath = path.join(__dirname, 'assets', 'tmp', sourceFile);
173
- console.log(bucketName, key);
174
- await downloadFileFromS3(bucketName, key, downloadPath);
175
-
176
- const fileNames = (subType === 'document' || subType === 'application')
177
- ? await convertPdfToImages(sourceFile.split('.')[0])
178
- : [downloadPath];
76
+ const maxRetries = polling?.maxRetries ?? DEFAULT_MAX_RETRIES;
77
+ let completed = false;
179
78
 
180
- if (subType === 'document' || subType === 'application') {
181
- await fs.promises.unlink(downloadPath);
182
- }
183
-
184
- console.log(fileNames);
185
-
186
- return fileNames;
187
- }
188
-
189
- async function processIndividualMessage(code, reply, provider, thread) {
190
- const tempFiles = [];
191
79
  try {
192
- const formattedMessage = formatMessage(reply);
193
- console.log('[processIndividualMessage] formattedMessage:', formattedMessage);
194
- const isPatient = reply.origin === 'patient';
195
- let messagesChat = [];
196
- let url = null;
197
-
198
- if (formattedMessage) {
199
- messagesChat.push({ type: 'text', text: formattedMessage });
80
+ console.log('[runAssistantAndWait] RUN ID', run.id, 'THREAD ID', thread.getConversationId(), 'ASSISTANT ID', thread.getAssistantId());
81
+ ({run, completed} = await provider.checkRunStatus(assistant, thread.getConversationId(), run.id, 0, maxRetries));
82
+ } finally {
83
+ if (filter) {
84
+ await Thread.updateOne(filter, { $set: { run_id: null } });
200
85
  }
86
+ }
201
87
 
202
- // Handle media if present
203
- if (reply.is_media) {
204
- console.log('IS MEDIA', reply.is_media);
205
- const fileNames = await downloadMediaAndCreateFile(code, reply);
206
- tempFiles.push(...fileNames);
207
- for (const fileName of fileNames) {
208
- console.log(fileName);
209
- // Skip WBMP images and stickers
210
- if (fileName.toLowerCase().includes('.wbmp') || fileName.toLowerCase().includes('sticker')) {
211
- console.log('Skipping WBMP image or sticker:', fileName);
212
- continue;
213
- }
214
- if (fileName.includes('image') || fileName.includes('document') || fileName.includes('application')) {
215
- let imageAnalysis = null;
216
- try {
217
- imageAnalysis = await analyzeImage(fileName);
218
- } catch (error) {
219
- console.warn('[assistantHelper] analyzeImage failed:', error?.message || error);
220
- }
221
- console.log(imageAnalysis);
222
- const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
223
- if (imageAnalysis?.medical_relevance) {
224
- url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
225
- }
226
- if (imageAnalysis?.has_table && imageAnalysis.table_data) {
227
- messagesChat.push({
228
- type: 'text',
229
- text: imageAnalysis.table_data,
230
- });
231
- } else if (imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
232
- messagesChat.push({
233
- type: 'text',
234
- text: imageAnalysis.medical_analysis,
235
- });
236
- } else {
237
- console.log('Add attachment');
238
- /*const file = await provider.uploadFile({
239
- file: fs.createReadStream(fileName),
240
- purpose: 'vision',
241
- });
242
- messagesChat.push({
243
- type: 'image_file',
244
- image_file: { file_id: file.id },
245
- });*/
246
- messagesChat.push({
247
- type: 'text',
248
- text: imageAnalysis.description,
249
- });
250
- }
251
- } else if (fileName.includes('audio')) {
252
- const audioTranscript = await provider.transcribeAudio({
253
- file: fs.createReadStream(fileName),
254
- responseFormat: 'text',
255
- language: 'es'
256
- });
257
- const transcriptText = audioTranscript?.text || audioTranscript;
258
- messagesChat.push({
259
- type: 'text',
260
- text: transcriptText,
261
- });
262
- }
263
- }
264
- }
88
+ if (!completed) {
89
+ return { run: run, completed: false, output: '' };
90
+ }
265
91
 
266
- console.log('[processIndividualMessage] messagesChat', messagesChat);
92
+ const output = await provider.getRunText({ threadId: thread.getConversationId(), runId: run.id, fallback: '' });
267
93
 
268
- const threadId = thread.getConversationId();
269
- if (reply.origin === 'whatsapp_platform') {
270
- await provider.addMessage({
271
- threadId,
272
- role: 'assistant',
273
- content: messagesChat
274
- });
275
- } else if (reply.origin === 'patient') {
276
- await provider.addMessage({
277
- threadId,
278
- role: 'user',
279
- content: messagesChat
280
- });
281
- }
94
+ return { completed: true, output };
95
+ };
282
96
 
283
- await Message.updateOne(
284
- { message_id: reply.message_id, timestamp: reply.timestamp },
285
- { $set: { assistant_id: thread.getAssistantId(), thread_id: threadId } }
286
- );
97
+ const executeAssistantAttempt = async (thread, assistant, runConfig, attemptNumber) => {
98
+ const result = await runAssistantAndWait({
99
+ thread,
100
+ assistant,
101
+ runConfig
102
+ });
103
+
104
+ console.log(`[executeAssistantAttempt] Attempt ${attemptNumber}: completed=${result.completed}, output=${result.output || '(empty)'}`);
105
+
106
+ return result;
107
+ };
287
108
 
288
- return {isPatient, url};
289
- } catch (err) {
290
- console.log(`Error inside process message ${err}`);
291
- } finally {
292
- if (tempFiles.length > 0) {
293
- await Promise.all(tempFiles.map(async (filePath) => {
294
- try {
295
- await fs.promises.unlink(filePath);
296
- } catch (error) {
297
- if (error?.code !== 'ENOENT') {
298
- console.warn('[processIndividualMessage] Failed to remove temp file:', filePath, error?.message || error);
299
- }
300
- }
301
- }));
302
- }
109
+ const runAssistantWithRetries = async (thread, assistant, runConfig, patientReply = null) => {
110
+ if (patientReply) {
111
+ assistant.setReplies(patientReply);
303
112
  }
304
- }
305
113
 
306
- function getCurRow(baseID, code) {
307
- if (code.endsWith('@g.us')) {
308
- return getRecordByFilter(baseID, 'estado_general', `FIND("${code}", {Group ID})`);
309
- } else {
310
- console.log(code, `FIND("${code.split('@')[0]}", {whatsapp_id})`);
311
- return getRecordByFilter(baseID, 'estado_general', `FIND("${code.split('@')[0]}", {whatsapp_id})`);
312
- }
313
- }
114
+ let run, output, completed;
115
+ let retries = 0;
116
+ const maxRetries = DEFAULT_MAX_RETRIES;
117
+
118
+ do {
119
+ retries++;
120
+ ({ run, output, completed } = await withTracing(
121
+ executeAssistantAttempt,
122
+ 'assistant_attempt',
123
+ (thread, assistant, runConfig, attemptNumber) => ({
124
+ 'attempt.number': attemptNumber,
125
+ 'attempt.is_retry': attemptNumber > 1,
126
+ 'attempt.max_attempts': maxRetries
127
+ })
128
+ )(thread, assistant, runConfig, retries));
129
+
130
+ if (completed && output) break;
131
+ if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
132
+ } while (retries < maxRetries && (!completed || !output));
133
+
134
+ if (run?.last_error) console.log('[runAssistantWithRetries] RUN LAST ERROR:', run.last_error);
135
+ console.log('[runAssistantWithRetries] RUN STATUS', completed);
136
+ console.log('[runAssistantWithRetries] OUTPUT', output);
137
+
138
+ return { run, output, completed, retries };
139
+ };
314
140
 
315
141
  module.exports = {
316
142
  checkIfFinished,
317
- getLastMessages,
318
- getLastNMessages,
319
- getPatientRoleAndName,
320
143
  getCurRow,
321
- formatMessage,
322
- downloadMediaAndCreateFile,
323
- processIndividualMessage
144
+ runAssistantAndWait,
145
+ runAssistantWithRetries,
146
+ executeAssistantAttempt
324
147
  };
@@ -1,5 +1,6 @@
1
1
  const { Message, insertMessage, getMessageValues } = require('../models/messageModel.js');
2
2
  const { uploadMediaToS3 } = require('./mediaHelper.js');
3
+ const { isRecentMessage } = require('./messageHelper.js');
3
4
  const { downloadMediaMessage } = require('baileys');
4
5
 
5
6
 
@@ -105,17 +106,6 @@ function extractContentTypeAndReply(message, messageType) {
105
106
  return { content, contentType, reply };
106
107
  }
107
108
 
108
- async function isRecentMessage(chatId) {
109
- const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
110
-
111
- const recentMessage = await Message.find({
112
- $or: [{ group_id: chatId }, { numero: chatId }],
113
- createdAt: { $gte: fiveMinutesAgo }
114
- }).sort({ createdAt: -1 }).limit(1);
115
-
116
- return !!recentMessage;
117
- }
118
-
119
109
  async function getLastMessages(chatId, n) {
120
110
  const messages = await Message.find({ group_id: chatId })
121
111
  .sort({ createdAt: -1 })
@@ -1,36 +1,41 @@
1
1
  const { PDFDocument } = require('pdf-lib');
2
- const { exec } = require('child_process');
3
- const fs = require('fs');
2
+ const { execFile } = require('child_process');
3
+ const fs = require('fs').promises;
4
4
  const path = require('path');
5
5
  const sharp = require('sharp');
6
6
 
7
+ const { downloadFileFromS3 } = require('../config/awsConfig.js');
8
+ const { Message } = require('../models/messageModel.js');
9
+ const { sanitizeFilename } = require('../utils/sanitizer.js');
7
10
 
8
11
  async function convertPdfToImages(pdfName) {
9
12
  const outputDir = path.join(__dirname, 'assets', 'tmp');
10
- const pdfPath = `${outputDir}/${pdfName}.pdf`;
13
+
14
+ const sanitizedName = sanitizeFilename(pdfName);
15
+ const pdfPath = path.join(outputDir, `${sanitizedName}.pdf`);
16
+ const outputPattern = path.join(outputDir, sanitizedName);
11
17
 
12
- if (!fs.existsSync(outputDir)) {
13
- fs.mkdirSync(outputDir, { recursive: true });
14
- }
18
+ await fs.promises.mkdir(outputDir, { recursive: true });
15
19
 
16
20
  return new Promise((resolve, reject) => {
17
- const command = `pdftoppm -jpeg ${pdfPath} ${outputDir}/${pdfName}`;
18
- console.log(command);
19
- exec(command, (error, stdout, stderr) => {
21
+ const args = ['-jpeg', pdfPath, outputPattern];
22
+ console.log('[convertPdfToImages] Running: pdftoppm', args.join(' '));
23
+
24
+ execFile('pdftoppm', args, (error, stdout, stderr) => {
20
25
  if (error) {
21
- return reject(`Error splitting PDF: ${stderr}`);
26
+ return reject(new Error(`Error splitting PDF: ${stderr || error.message}`));
22
27
  }
23
28
 
24
29
  fs.readdir(outputDir, (err, files) => {
25
30
  if (err) {
26
- return reject(`Error reading output directory: ${err.message}`);
31
+ return reject(new Error(`Error reading output directory: ${err.message}`));
27
32
  }
28
33
 
29
- const pngFiles = files
30
- .filter(file => file.startsWith(pdfName) && file.endsWith('.jpg'))
34
+ const jpgFiles = files
35
+ .filter(file => file.startsWith(sanitizedName) && file.endsWith('.jpg'))
31
36
  .map(file => path.join(outputDir, file));
32
37
 
33
- resolve(pngFiles);
38
+ resolve(jpgFiles);
34
39
  });
35
40
  });
36
41
  });
@@ -43,11 +48,9 @@ async function combineImagesToPDF(config) {
43
48
  sortNumerically = true
44
49
  } = config;
45
50
 
46
- // Get all files in the directory
47
51
  const inputDir = path.join(__dirname, 'assets', 'tmp');
48
52
  const files = await fs.promises.readdir(inputDir);
49
53
 
50
- // Filter for image files with the specified extensions
51
54
  const imageFiles = files.filter(file => {
52
55
  const ext = path.extname(file).toLowerCase().substring(1);
53
56
  const hasValidExtension = extensions.includes(ext);
@@ -55,7 +58,6 @@ async function combineImagesToPDF(config) {
55
58
  return hasValidExtension && hasPrefix;
56
59
  });
57
60
 
58
- // Sort files
59
61
  if (sortNumerically) {
60
62
  imageFiles.sort((a, b) => {
61
63
  const aMatch = a.match(/\d+/g);
@@ -71,11 +73,9 @@ async function combineImagesToPDF(config) {
71
73
 
72
74
  console.log(`Found ${imageFiles.length} image files to combine`);
73
75
 
74
- // Create a new PDF document
75
76
  const pdfDoc = await PDFDocument.create();
76
77
  const processedFiles = [];
77
78
 
78
- // Process each image file
79
79
  for (const [index, file] of imageFiles.entries()) {
80
80
  try {
81
81
  const filePath = path.join(inputDir, file);
@@ -89,8 +89,7 @@ async function combineImagesToPDF(config) {
89
89
  const { width, height } = await sharp(imageBuffer).metadata();
90
90
  const img = await pdfDoc.embedPng(pngBuffer);
91
91
  const page = pdfDoc.addPage([width, height]);
92
-
93
- // Draw the image on the page
92
+
94
93
  page.drawImage(img, {
95
94
  x: 0,
96
95
  y: 0,
@@ -112,23 +111,67 @@ async function combineImagesToPDF(config) {
112
111
  };
113
112
  }
114
113
 
115
- function cleanupFiles(files) {
116
- files.forEach(file => {
117
- if (fs.existsSync(file)) {
118
- try {
119
- fs.unlinkSync(file);
120
- console.log(`Deleted: ${file}`);
121
- } catch (error) {
122
- console.error(`Error deleting ${file}:`, error);
114
+ const cleanupFiles = async (files) => {
115
+ if (!files || files.length === 0) return;
116
+
117
+ await Promise.all(files.map(async (filePath) => {
118
+ try {
119
+ await fs.promises.unlink(filePath);
120
+ } catch (error) {
121
+ if (error?.code !== 'ENOENT') {
122
+ const safeFileName = filePath ? filePath.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown';
123
+ console.warn(`[cleanupFiles] Error deleting ${safeFileName}:`, error?.message || String(error));
123
124
  }
124
- } else {
125
- console.warn(`File not found: ${file}`);
126
125
  }
126
+ }));
127
+
128
+ console.log(`[cleanupFiles] Cleaned up ${files.length} files`);
129
+ };
130
+
131
+ async function downloadMediaAndCreateFile(code, reply) {
132
+ const resultMedia = await Message.findOne({
133
+ message_id: reply.message_id,
134
+ timestamp: reply.timestamp,
135
+ media: { $ne: null }
127
136
  });
137
+
138
+ if (!resultMedia) return [];
139
+
140
+ if (!resultMedia.media || !resultMedia.media.key) {
141
+ console.log('[downloadMediaAndCreateFile] No valid media found for message:', reply.message_id);
142
+ return [];
143
+ }
144
+
145
+ const { bucketName, key } = resultMedia.media;
146
+ if (!bucketName || !key) return [];
147
+
148
+ const [subType, fileName] = key.split('/');
149
+
150
+ const sanitizedCode = sanitizeFilename(code);
151
+ const sanitizedSubType = sanitizeFilename(subType);
152
+ const sanitizedFileName = sanitizeFilename(fileName);
153
+
154
+ const sourceFile = `${sanitizedCode}-${sanitizedSubType}-${sanitizedFileName}`;
155
+ const downloadPath = path.join(__dirname, 'assets', 'tmp', sourceFile);
156
+
157
+ await fs.promises.mkdir(path.dirname(downloadPath), { recursive: true });
158
+ await downloadFileFromS3(bucketName, key, downloadPath);
159
+
160
+ const { name: baseName } = path.parse(sourceFile);
161
+ const fileNames = (subType === 'document' || subType === 'application')
162
+ ? await convertPdfToImages(baseName)
163
+ : [downloadPath];
164
+
165
+ if (subType === 'document' || subType === 'application') {
166
+ await fs.promises.unlink(downloadPath);
167
+ }
168
+
169
+ return fileNames;
128
170
  }
129
171
 
130
172
  module.exports = {
131
173
  convertPdfToImages,
132
174
  combineImagesToPDF,
133
- cleanupFiles
175
+ cleanupFiles,
176
+ downloadMediaAndCreateFile
134
177
  };
@@ -1,10 +1,11 @@
1
- const AWS = require('../config/awsConfig.js');
2
1
  const path = require('path');
3
2
  const fs = require('fs');
3
+ const AWS = require('../config/awsConfig.js');
4
+ const { sanitizeMediaFilename } = require('../utils/sanitizer.js');
4
5
 
5
6
  async function uploadMediaToS3(buffer, messageID, titleFile, bucketName, contentType, messageType) {
6
7
  const extension = getFileExtension(contentType);
7
- const sanitizedTitle = titleFile ? sanitizeFileName(titleFile) : '';
8
+ const sanitizedTitle = titleFile ? sanitizeMediaFilename(titleFile) : '';
8
9
  const fileName = sanitizedTitle
9
10
  ? `${messageType}/${messageID}_${sanitizedTitle}.${extension}`
10
11
  : `${messageType}/${messageID}.${extension}`;
@@ -58,15 +59,8 @@ function getFileExtension(contentType) {
58
59
  return mimeToExt[contentType] || 'bin';
59
60
  }
60
61
 
61
- function sanitizeFileName(fileName) {
62
- return fileName
63
- .replace(/[^a-zA-Z0-9_-]/g, '')
64
- .slice(0, 50);
65
- }
66
-
67
62
  module.exports = {
68
63
  uploadMediaToS3,
69
64
  saveMediaToLocal,
70
- getFileExtension,
71
- sanitizeFileName
65
+ getFileExtension
72
66
  };