@peopl-health/nexus 2.4.8 → 2.4.9-fix-mime-type
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/helpers/filesHelper.js +100 -44
- package/lib/helpers/llmsHelper.js +31 -7
- package/lib/helpers/processHelper.js +1 -1
- package/lib/providers/OpenAIResponsesProvider.js +6 -0
- package/lib/services/assistantService.js +54 -38
- package/lib/utils/mediaValidator.js +18 -14
- package/lib/utils/tracingDecorator.js +7 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { PDFDocument } = require('pdf-lib');
|
|
2
2
|
const { execFile } = require('child_process');
|
|
3
3
|
const fs = require('fs').promises;
|
|
4
|
+
const fsSync = require('fs');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const sharp = require('sharp');
|
|
6
7
|
|
|
@@ -9,11 +10,11 @@ const { Message } = require('../models/messageModel.js');
|
|
|
9
10
|
const { sanitizeFilename } = require('../utils/sanitizer.js');
|
|
10
11
|
const { logger } = require('../utils/logger');
|
|
11
12
|
|
|
12
|
-
async function convertPdfToImages(pdfName) {
|
|
13
|
+
async function convertPdfToImages(pdfName, existingPdfPath = null) {
|
|
13
14
|
const outputDir = path.join(__dirname, 'assets', 'tmp');
|
|
14
15
|
|
|
15
16
|
const sanitizedName = sanitizeFilename(pdfName);
|
|
16
|
-
const pdfPath = path.join(outputDir, `${sanitizedName}.pdf`);
|
|
17
|
+
const pdfPath = existingPdfPath || path.join(outputDir, `${sanitizedName}.pdf`);
|
|
17
18
|
const outputPattern = path.join(outputDir, sanitizedName);
|
|
18
19
|
|
|
19
20
|
await fs.mkdir(outputDir, { recursive: true });
|
|
@@ -22,22 +23,52 @@ async function convertPdfToImages(pdfName) {
|
|
|
22
23
|
const args = ['-jpeg', pdfPath, outputPattern];
|
|
23
24
|
logger.info('[convertPdfToImages] Running: pdftoppm', args.join(' '));
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
const timeout = 30000;
|
|
27
|
+
let timedOut = false;
|
|
28
|
+
|
|
29
|
+
const child = execFile('pdftoppm', args, { timeout, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
30
|
+
if (timedOut) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
if (error) {
|
|
35
|
+
logger.error('[convertPdfToImages] Error details:', {
|
|
36
|
+
error: error.message,
|
|
37
|
+
stderr,
|
|
38
|
+
pdfPath,
|
|
39
|
+
pdfExists: fsSync.existsSync(pdfPath),
|
|
40
|
+
killed: error.killed,
|
|
41
|
+
signal: error.signal
|
|
42
|
+
});
|
|
27
43
|
return reject(new Error(`Error splitting PDF: ${stderr || error.message}`));
|
|
28
44
|
}
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
logger.info('[convertPdfToImages] pdftoppm completed successfully');
|
|
47
|
+
|
|
48
|
+
fs.readdir(outputDir)
|
|
49
|
+
.then(files => {
|
|
50
|
+
const jpgFiles = files
|
|
51
|
+
.filter(file => file.startsWith(sanitizedName) && file.endsWith('.jpg'))
|
|
52
|
+
.map(file => path.join(outputDir, file));
|
|
53
|
+
|
|
54
|
+
logger.info(`[convertPdfToImages] Found ${jpgFiles.length} image files`);
|
|
55
|
+
resolve(jpgFiles);
|
|
56
|
+
})
|
|
57
|
+
.catch(err => {
|
|
58
|
+
logger.error('[convertPdfToImages] Error reading output directory:', { error: err.message });
|
|
59
|
+
reject(new Error(`Error reading output directory: ${err.message}`));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
34
62
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
const timeoutId = setTimeout(() => {
|
|
64
|
+
timedOut = true;
|
|
65
|
+
child.kill('SIGTERM');
|
|
66
|
+
logger.error('[convertPdfToImages] Process timed out after 30 seconds', { pdfPath });
|
|
67
|
+
reject(new Error('PDF conversion timed out after 30 seconds'));
|
|
68
|
+
}, timeout);
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
child.on('exit', () => {
|
|
71
|
+
clearTimeout(timeoutId);
|
|
41
72
|
});
|
|
42
73
|
});
|
|
43
74
|
}
|
|
@@ -130,44 +161,69 @@ const cleanupFiles = async (files) => {
|
|
|
130
161
|
};
|
|
131
162
|
|
|
132
163
|
async function downloadMediaAndCreateFile(code, reply) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
try {
|
|
165
|
+
const resultMedia = await Message.findOne({
|
|
166
|
+
message_id: reply.message_id,
|
|
167
|
+
timestamp: reply.timestamp,
|
|
168
|
+
media: { $ne: null }
|
|
169
|
+
});
|
|
138
170
|
|
|
139
|
-
|
|
171
|
+
if (!resultMedia) return [];
|
|
140
172
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
if (!resultMedia.media || !resultMedia.media.key) {
|
|
174
|
+
logger.info('[downloadMediaAndCreateFile] No valid media found for message:', reply.message_id);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
145
177
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
178
|
+
const { bucketName, key } = resultMedia.media;
|
|
179
|
+
if (!bucketName || !key) return [];
|
|
180
|
+
|
|
181
|
+
const [subType, fileName] = key.split('/');
|
|
182
|
+
|
|
183
|
+
const sanitizedCode = sanitizeFilename(code, 20);
|
|
184
|
+
const sanitizedSubType = sanitizeFilename(subType, 10);
|
|
185
|
+
const sanitizedFileName = sanitizeFilename(fileName, 50);
|
|
186
|
+
|
|
187
|
+
const sourceFile = `${sanitizedCode}-${sanitizedSubType}-${sanitizedFileName}`;
|
|
188
|
+
const downloadPath = path.join(__dirname, 'assets', 'tmp', sourceFile);
|
|
189
|
+
|
|
190
|
+
logger.info('[downloadMediaAndCreateFile] Downloading file', { sourceFile, downloadPath, bucketName, key });
|
|
191
|
+
|
|
192
|
+
await fs.mkdir(path.dirname(downloadPath), { recursive: true });
|
|
193
|
+
await downloadFileFromS3(bucketName, key, downloadPath);
|
|
160
194
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
195
|
+
const { name: baseName } = path.parse(sourceFile);
|
|
196
|
+
let fileNames = [];
|
|
197
|
+
|
|
198
|
+
if (subType === 'document' || subType === 'application') {
|
|
199
|
+
try {
|
|
200
|
+
fileNames = await convertPdfToImages(baseName, downloadPath);
|
|
201
|
+
logger.info('[downloadMediaAndCreateFile] PDF converted successfully', { imageCount: fileNames.length });
|
|
202
|
+
} catch (conversionError) {
|
|
203
|
+
logger.error('[downloadMediaAndCreateFile] PDF conversion failed:', {
|
|
204
|
+
error: conversionError.message,
|
|
205
|
+
sourceFile
|
|
206
|
+
});
|
|
207
|
+
fileNames = [];
|
|
208
|
+
} finally {
|
|
209
|
+
try {
|
|
210
|
+
await fs.unlink(downloadPath);
|
|
211
|
+
} catch (unlinkError) {
|
|
212
|
+
logger.warn('[downloadMediaAndCreateFile] Failed to delete PDF:', { error: unlinkError.message });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
fileNames = [downloadPath];
|
|
217
|
+
}
|
|
165
218
|
|
|
166
|
-
|
|
167
|
-
|
|
219
|
+
return fileNames;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logger.error('[downloadMediaAndCreateFile] Error processing media:', {
|
|
222
|
+
error: error.message,
|
|
223
|
+
message_id: reply.message_id
|
|
224
|
+
});
|
|
225
|
+
return [];
|
|
168
226
|
}
|
|
169
|
-
|
|
170
|
-
return fileNames;
|
|
171
227
|
}
|
|
172
228
|
|
|
173
229
|
module.exports = {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const llmConfig = require('../config/llmConfig.js');
|
|
2
2
|
const { logger } = require('../utils/logger');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const
|
|
4
|
+
const path = require('path');
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
async function analyzeImage(imagePath, isSticker = false) {
|
|
7
|
+
async function analyzeImage(imagePath, isSticker = false, contentType = null) {
|
|
8
8
|
try {
|
|
9
9
|
const anthropicClient = llmConfig.anthropicClient;
|
|
10
10
|
if (!anthropicClient || !anthropicClient.messages) {
|
|
@@ -30,8 +30,30 @@ async function analyzeImage(imagePath, isSticker = false) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
|
|
33
|
+
// Determine mime type from file extension
|
|
34
|
+
let mimeType = contentType;
|
|
35
|
+
if (!mimeType) {
|
|
36
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
37
|
+
const mimeMap = {
|
|
38
|
+
'.jpg': 'image/jpeg',
|
|
39
|
+
'.jpeg': 'image/jpeg',
|
|
40
|
+
'.png': 'image/png',
|
|
41
|
+
'.gif': 'image/gif',
|
|
42
|
+
'.webp': 'image/webp'
|
|
43
|
+
};
|
|
44
|
+
mimeType = mimeMap[ext] || 'image/jpeg'; // Default to jpeg for pdftoppm output
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Validate that mime type is supported by Claude
|
|
48
|
+
const supportedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
49
|
+
if (!supportedMimeTypes.includes(mimeType)) {
|
|
50
|
+
logger.warn('[analyzeImage] Unsupported mime type, defaulting to image/jpeg:', {
|
|
51
|
+
originalMimeType: mimeType,
|
|
52
|
+
imagePath
|
|
53
|
+
});
|
|
54
|
+
mimeType = 'image/jpeg';
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
if (mimeType === 'image/vnd.wap.wbmp') {
|
|
36
58
|
logger.info('Skipping image with MIME type:', mimeType);
|
|
37
59
|
return {
|
|
@@ -43,6 +65,7 @@ async function analyzeImage(imagePath, isSticker = false) {
|
|
|
43
65
|
};
|
|
44
66
|
}
|
|
45
67
|
// Read the image file and convert to base64
|
|
68
|
+
logger.info('[analyzeImage] Reading image file:', { imagePath: imagePath.split('/').pop() });
|
|
46
69
|
const imageBuffer = await fs.promises.readFile(imagePath);
|
|
47
70
|
const base64Image = imageBuffer.toString('base64');
|
|
48
71
|
|
|
@@ -71,6 +94,7 @@ async function analyzeImage(imagePath, isSticker = false) {
|
|
|
71
94
|
},
|
|
72
95
|
],
|
|
73
96
|
});
|
|
97
|
+
logger.info('[analyzeImage] Description received');
|
|
74
98
|
const description = messageDescription.content[0].text;
|
|
75
99
|
|
|
76
100
|
// For stickers, skip medical analysis and table extraction
|
|
@@ -114,7 +138,7 @@ Only extract tables - ignore any other content in the image.`;
|
|
|
114
138
|
type: 'image',
|
|
115
139
|
source: {
|
|
116
140
|
type: 'base64',
|
|
117
|
-
media_type:
|
|
141
|
+
media_type: mimeType,
|
|
118
142
|
data: base64Image,
|
|
119
143
|
},
|
|
120
144
|
},
|
|
@@ -181,7 +205,7 @@ Ejemplo 1:
|
|
|
181
205
|
type: 'image',
|
|
182
206
|
source: {
|
|
183
207
|
type: 'base64',
|
|
184
|
-
media_type:
|
|
208
|
+
media_type: mimeType,
|
|
185
209
|
data: base64Image,
|
|
186
210
|
},
|
|
187
211
|
},
|
|
@@ -209,7 +233,7 @@ Ejemplo 1:
|
|
|
209
233
|
type: 'image',
|
|
210
234
|
source: {
|
|
211
235
|
type: 'base64',
|
|
212
|
-
media_type:
|
|
236
|
+
media_type: mimeType,
|
|
213
237
|
data: base64Image,
|
|
214
238
|
},
|
|
215
239
|
},
|
|
@@ -66,7 +66,7 @@ const processImageFile = async (fileName, reply) => {
|
|
|
66
66
|
fileName.toLowerCase().includes('/sticker/');
|
|
67
67
|
|
|
68
68
|
try {
|
|
69
|
-
imageAnalysis = await analyzeImage(fileName, isSticker);
|
|
69
|
+
imageAnalysis = await analyzeImage(fileName, isSticker, reply.media?.contentType);
|
|
70
70
|
|
|
71
71
|
logger.info('processImageFile', {
|
|
72
72
|
message_id: reply.message_id,
|
|
@@ -212,6 +212,12 @@ class OpenAIResponsesProvider {
|
|
|
212
212
|
|
|
213
213
|
if (payloads.length === 0) return null;
|
|
214
214
|
|
|
215
|
+
if (payloads.length > MAX_ITEMS_PER_BATCH) {
|
|
216
|
+
logger.info(`[OpenAIResponsesProvider] Batching ${payloads.length} messages into chunks of ${MAX_ITEMS_PER_BATCH}`);
|
|
217
|
+
await this._addItemsInBatches(id, payloads, MAX_ITEMS_PER_BATCH);
|
|
218
|
+
return { batched: true, count: payloads.length };
|
|
219
|
+
}
|
|
220
|
+
|
|
215
221
|
return this._retryWithRateLimit(async () => {
|
|
216
222
|
if (this.conversations?.items?.create) {
|
|
217
223
|
return await this.conversations.items.create(id, { items: payloads });
|
|
@@ -273,21 +273,28 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
273
273
|
const timings = {};
|
|
274
274
|
const startTotal = Date.now();
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
const { result: thread, duration: getThreadMs } = await withTracing(
|
|
277
|
+
getThread,
|
|
278
|
+
'get_thread_operation',
|
|
278
279
|
(threadCode) => ({
|
|
279
280
|
'thread.code': threadCode,
|
|
280
281
|
'operation.type': 'thread_retrieval',
|
|
281
282
|
'thread.provided': !!thread_
|
|
282
|
-
})
|
|
283
|
+
}),
|
|
284
|
+
{ returnTiming: true }
|
|
283
285
|
)(code);
|
|
284
|
-
timings.
|
|
286
|
+
timings.get_thread_ms = getThreadMs;
|
|
285
287
|
|
|
286
|
-
if (!thread) return null;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const patientReply = await
|
|
290
|
-
|
|
288
|
+
if (!thread_ && !thread) return null;
|
|
289
|
+
const finalThread = thread_ || thread;
|
|
290
|
+
|
|
291
|
+
const { result: patientReply, duration: getMessagesMs } = await withTracing(
|
|
292
|
+
getLastMessages,
|
|
293
|
+
'get_last_messages',
|
|
294
|
+
(code) => ({ 'thread.code': code }),
|
|
295
|
+
{ returnTiming: true }
|
|
296
|
+
)(code);
|
|
297
|
+
timings.get_messages_ms = getMessagesMs;
|
|
291
298
|
|
|
292
299
|
if (!patientReply) {
|
|
293
300
|
logger.info('[replyAssistantCore] No relevant data found for this assistant.');
|
|
@@ -296,10 +303,18 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
296
303
|
|
|
297
304
|
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
298
305
|
|
|
299
|
-
timings.processMessages = Date.now();
|
|
300
306
|
logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
|
|
301
307
|
|
|
302
|
-
const processResults = await
|
|
308
|
+
const { result: processResults, duration: processMessagesMs } = await withTracing(
|
|
309
|
+
processThreadMessage,
|
|
310
|
+
'process_thread_messages',
|
|
311
|
+
(code, patientReply, provider) => ({
|
|
312
|
+
'messages.count': patientReply.length,
|
|
313
|
+
'thread.code': code
|
|
314
|
+
}),
|
|
315
|
+
{ returnTiming: true }
|
|
316
|
+
)(code, patientReply, provider);
|
|
317
|
+
timings.process_messages_ms = processMessagesMs;
|
|
303
318
|
|
|
304
319
|
const patientMsg = processResults.some(r => r.isPatient);
|
|
305
320
|
const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
|
|
@@ -307,21 +322,27 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
307
322
|
const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
|
|
308
323
|
|
|
309
324
|
if (allMessagesToAdd.length > 0) {
|
|
310
|
-
const threadId =
|
|
325
|
+
const threadId = finalThread.getConversationId();
|
|
311
326
|
logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
|
|
312
327
|
await provider.addMessage({ threadId, messages: allMessagesToAdd });
|
|
313
328
|
}
|
|
314
329
|
|
|
315
|
-
await Promise.all(processResults.map(r => updateMessageRecord(r.reply,
|
|
330
|
+
await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
|
|
316
331
|
await cleanupFiles(allTempFiles);
|
|
317
332
|
|
|
318
|
-
timings.processMessages = Date.now() - timings.processMessages;
|
|
319
|
-
|
|
320
333
|
if (urls.length > 0) {
|
|
321
|
-
timings.pdfCombination = Date.now();
|
|
322
334
|
logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
|
|
323
|
-
const {
|
|
324
|
-
|
|
335
|
+
const { result: pdfResult, duration: pdfCombinationMs } = await withTracing(
|
|
336
|
+
combineImagesToPDF,
|
|
337
|
+
'combine_images_to_pdf',
|
|
338
|
+
({ code }) => ({
|
|
339
|
+
'pdf.thread_code': code,
|
|
340
|
+
'pdf.url_count': urls.length
|
|
341
|
+
}),
|
|
342
|
+
{ returnTiming: true }
|
|
343
|
+
)({ code });
|
|
344
|
+
timings.pdf_combination_ms = pdfCombinationMs;
|
|
345
|
+
const { pdfBuffer, processedFiles } = pdfResult;
|
|
325
346
|
logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
|
|
326
347
|
|
|
327
348
|
if (pdfBuffer) {
|
|
@@ -337,47 +358,42 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
337
358
|
}
|
|
338
359
|
}
|
|
339
360
|
|
|
340
|
-
if (!patientMsg ||
|
|
361
|
+
if (!patientMsg || finalThread.stopped) return null;
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const { run, output, completed, retries, predictionTimeMs } = await withTracing(
|
|
363
|
+
const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
|
|
364
|
+
const { result: runResult, duration: runAssistantMs } = await withTracing(
|
|
345
365
|
runAssistantWithRetries,
|
|
346
366
|
'run_assistant_with_retries',
|
|
347
367
|
(thread, assistant, runConfig, patientReply) => ({
|
|
348
368
|
'assistant.id': thread.getAssistantId(),
|
|
349
369
|
'assistant.max_retries': DEFAULT_MAX_RETRIES,
|
|
350
370
|
'assistant.has_patient_reply': !!patientReply
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
timings.
|
|
371
|
+
}),
|
|
372
|
+
{ returnTiming: true }
|
|
373
|
+
)(finalThread, assistant, runOptions, patientReply);
|
|
374
|
+
timings.run_assistant_ms = runAssistantMs;
|
|
375
|
+
timings.total_ms = Date.now() - startTotal;
|
|
376
|
+
|
|
377
|
+
const { run, output, completed, retries, predictionTimeMs } = runResult;
|
|
355
378
|
|
|
356
|
-
logger.info('[
|
|
379
|
+
logger.info('[Assistant Reply Complete]', {
|
|
357
380
|
code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
|
|
358
381
|
messageCount: patientReply.length,
|
|
359
382
|
hasMedia: urls.length > 0,
|
|
360
383
|
retries,
|
|
361
|
-
|
|
384
|
+
totalMs: timings.total_ms
|
|
362
385
|
});
|
|
363
386
|
|
|
364
387
|
if (output && predictionTimeMs) {
|
|
365
388
|
await PredictionMetrics.create({
|
|
366
389
|
message_id: `${code}-${Date.now()}`,
|
|
367
390
|
numero: code,
|
|
368
|
-
assistant_id:
|
|
369
|
-
thread_id:
|
|
391
|
+
assistant_id: finalThread.getAssistantId(),
|
|
392
|
+
thread_id: finalThread.getConversationId(),
|
|
370
393
|
prediction_time_ms: predictionTimeMs,
|
|
371
394
|
retry_count: retries,
|
|
372
395
|
completed: completed,
|
|
373
|
-
timing_breakdown:
|
|
374
|
-
get_thread_ms: timings.getThread,
|
|
375
|
-
get_messages_ms: timings.getMessages,
|
|
376
|
-
process_messages_ms: timings.processMessages,
|
|
377
|
-
pdf_combination_ms: timings.pdfCombination || 0,
|
|
378
|
-
run_assistant_ms: timings.runAssistant,
|
|
379
|
-
total_ms: timings.total
|
|
380
|
-
}
|
|
396
|
+
timing_breakdown: timings
|
|
381
397
|
}).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
|
|
382
398
|
}
|
|
383
399
|
|
|
@@ -65,23 +65,27 @@ function getMediaType(contentType) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function validateMedia(media, contentType) {
|
|
68
|
+
const fileSize = Buffer.isBuffer(media) ? media.length : media;
|
|
69
|
+
|
|
70
|
+
if (contentType === 'image/webp') {
|
|
71
|
+
const mediaType = fileSize <= MEDIA_LIMITS.sticker ? 'sticker' : 'image';
|
|
72
|
+
const formatValidation = validateMediaFormat(contentType, mediaType);
|
|
73
|
+
if (!formatValidation.valid) return formatValidation;
|
|
74
|
+
|
|
75
|
+
const sizeValidation = validateMediaSize(media, mediaType);
|
|
76
|
+
if (!sizeValidation.valid) return sizeValidation;
|
|
77
|
+
|
|
78
|
+
return { valid: true, mediaType, message: `Media validated successfully as ${mediaType}` };
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
const mediaType = getMediaType(contentType);
|
|
69
82
|
const formatValidation = validateMediaFormat(contentType, mediaType);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return formatValidation;
|
|
73
|
-
}
|
|
74
|
-
|
|
83
|
+
if (!formatValidation.valid) return formatValidation;
|
|
84
|
+
|
|
75
85
|
const sizeValidation = validateMediaSize(media, mediaType);
|
|
76
|
-
if (!sizeValidation.valid)
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
valid: true,
|
|
82
|
-
mediaType,
|
|
83
|
-
message: `Media validated successfully as ${mediaType}`
|
|
84
|
-
};
|
|
86
|
+
if (!sizeValidation.valid) return sizeValidation;
|
|
87
|
+
|
|
88
|
+
return { valid: true, mediaType, message: `Media validated successfully as ${mediaType}` };
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
module.exports = {
|
|
@@ -4,9 +4,10 @@ const { SpanStatusCode } = require('@opentelemetry/api');
|
|
|
4
4
|
/**
|
|
5
5
|
* Usage: const tracedFunction = withTracing(originalFunction, 'operation_name');
|
|
6
6
|
*/
|
|
7
|
-
const withTracing = (fn, spanName, attributeMapper = null) => {
|
|
7
|
+
const withTracing = (fn, spanName, attributeMapper = null, options = {}) => {
|
|
8
8
|
return async function (...args) {
|
|
9
9
|
const span = createSpan(spanName);
|
|
10
|
+
const startTime = Date.now();
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
if (attributeMapper && typeof attributeMapper === 'function') {
|
|
@@ -16,6 +17,11 @@ const withTracing = (fn, spanName, attributeMapper = null) => {
|
|
|
16
17
|
const result = await fn.apply(this, args);
|
|
17
18
|
|
|
18
19
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
20
|
+
|
|
21
|
+
if (options.returnTiming) {
|
|
22
|
+
const duration = Date.now() - startTime;
|
|
23
|
+
return { result, duration };
|
|
24
|
+
}
|
|
19
25
|
return result;
|
|
20
26
|
|
|
21
27
|
} catch (error) {
|