@runnerpro/backend 1.17.1 → 1.17.3

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 CHANGED
@@ -1,82 +1,82 @@
1
- # modern-npm-package
2
-
3
- An npm packages for common backend logic between client and dashboard app.
4
-
5
- ## Get Started
6
-
7
- 1. Run `npm install @runnerpro/backend`
8
-
9
- ## Functions
10
-
11
- ### query
12
-
13
- Connection with the PostgresSQL database
14
-
15
- - <b>Param</b>: string with PostgreSQL query
16
- - <b>Param</b>: array of values
17
- - <b>Return</b>: promise of array of values (each item is a row of the table)
18
-
19
- ### batchQuery
20
-
21
- Connection with PostgreSQL database in batches. Use for faster query execution when we need to execute many queries.
22
-
23
- - <b>Param</b>: Array of queries
24
- - query: string with PostgreSQL query
25
- - values: array of values
26
- - <b>Param?</b>: length of promises wait together. Default 5
27
- - <b>Return</b>: promise (without values)
28
-
29
- ### sendMail
30
-
31
- Send mail with company logo and template
32
-
33
- - <b>Param</b>: { subject, title, body, to, link?, attachments?, bcc? }
34
-
35
- - subject: title of the mail
36
- - body: text inside card
37
- - to: array of mails. In DEV and SBX the value is taken from .env GMAIL_EMAIL_USER
38
- - link: button to redirect to the app. { text: text inside button, url: path (after runnerpro.com) }
39
- - attachments: array of files to attach. { filename: name and extension of file, path: path of file, cid?: identifier to insert inside the body (`<img src="cid:__cid__" />`) }
40
- - bcc: array of mails to send via bcc
41
-
42
- - <b>Return</b>: promise (without values)
43
-
44
- ### sendNotification
45
-
46
- Send web or native notification to client: TODO
47
-
48
- - <b>Param</b>: Array of queries { query: string with PostgreSQL query, values: array of values }
49
- - <b>Param?</b>: length of promises wait together. Default 5
50
- - <b>Return</b>: promise (without values)
51
-
52
- ### sleep
53
-
54
- Wait X miliseconds to execute next line
55
-
56
- - <b>Param</b>: number of miliseconds of the promise
57
- - <b>Return</b>: promise (without values)
58
-
59
- ## Deploy new version
60
-
61
- 1. Create a folder with the name of functionality and index.ts inside. Write the function and export it.
62
- 2. In main index.ts (the one inside src), import and export it
63
- 3. Change the version number of the package ([using this convenction](https://docs.npmjs.com/about-semantic-versioning))
64
- 4. Run `npm login`
65
- 5. Run `npm run publish`
66
-
67
- ### Testing
68
-
69
- 1. Install developer dependencies using the following command in your terminal `npm i -D mocha @types/mocha chai @types/chai ts-node`
70
- 1. Create a new file `.mocharc.json` in the root directory with the following contents:
71
- ```json
72
- {
73
- "extension": ["ts"],
74
- "spec": "./**/*.spec.ts",
75
- "require": "ts-node/register"
76
- }
77
- ```
78
- 1. Create a `tests` folder
79
- 1. Create an `index.spec.ts` file in the `tests` folder
80
- 1. Write unit tests in the `index.spec.ts` file to test the code in `index.ts`
81
- 1. Add a `"test"` property in the `package.json` file and give it a value of `"mocha"`
82
- 1. Run `npm test` in your terminal from the root folder of the project
1
+ # modern-npm-package
2
+
3
+ An npm packages for common backend logic between client and dashboard app.
4
+
5
+ ## Get Started
6
+
7
+ 1. Run `npm install @runnerpro/backend`
8
+
9
+ ## Functions
10
+
11
+ ### query
12
+
13
+ Connection with the PostgresSQL database
14
+
15
+ - <b>Param</b>: string with PostgreSQL query
16
+ - <b>Param</b>: array of values
17
+ - <b>Return</b>: promise of array of values (each item is a row of the table)
18
+
19
+ ### batchQuery
20
+
21
+ Connection with PostgreSQL database in batches. Use for faster query execution when we need to execute many queries.
22
+
23
+ - <b>Param</b>: Array of queries
24
+ - query: string with PostgreSQL query
25
+ - values: array of values
26
+ - <b>Param?</b>: length of promises wait together. Default 5
27
+ - <b>Return</b>: promise (without values)
28
+
29
+ ### sendMail
30
+
31
+ Send mail with company logo and template
32
+
33
+ - <b>Param</b>: { subject, title, body, to, link?, attachments?, bcc? }
34
+
35
+ - subject: title of the mail
36
+ - body: text inside card
37
+ - to: array of mails. In DEV and SBX the value is taken from .env GMAIL_EMAIL_USER
38
+ - link: button to redirect to the app. { text: text inside button, url: path (after runnerpro.com) }
39
+ - attachments: array of files to attach. { filename: name and extension of file, path: path of file, cid?: identifier to insert inside the body (`<img src="cid:__cid__" />`) }
40
+ - bcc: array of mails to send via bcc
41
+
42
+ - <b>Return</b>: promise (without values)
43
+
44
+ ### sendNotification
45
+
46
+ Send web or native notification to client: TODO
47
+
48
+ - <b>Param</b>: Array of queries { query: string with PostgreSQL query, values: array of values }
49
+ - <b>Param?</b>: length of promises wait together. Default 5
50
+ - <b>Return</b>: promise (without values)
51
+
52
+ ### sleep
53
+
54
+ Wait X miliseconds to execute next line
55
+
56
+ - <b>Param</b>: number of miliseconds of the promise
57
+ - <b>Return</b>: promise (without values)
58
+
59
+ ## Deploy new version
60
+
61
+ 1. Create a folder with the name of functionality and index.ts inside. Write the function and export it.
62
+ 2. In main index.ts (the one inside src), import and export it
63
+ 3. Change the version number of the package ([using this convenction](https://docs.npmjs.com/about-semantic-versioning))
64
+ 4. Run `npm login`
65
+ 5. Run `npm run publish`
66
+
67
+ ### Testing
68
+
69
+ 1. Install developer dependencies using the following command in your terminal `npm i -D mocha @types/mocha chai @types/chai ts-node`
70
+ 1. Create a new file `.mocharc.json` in the root directory with the following contents:
71
+ ```json
72
+ {
73
+ "extension": ["ts"],
74
+ "spec": "./**/*.spec.ts",
75
+ "require": "ts-node/register"
76
+ }
77
+ ```
78
+ 1. Create a `tests` folder
79
+ 1. Create an `index.spec.ts` file in the `tests` folder
80
+ 1. Write unit tests in the `index.spec.ts` file to test the code in `index.ts`
81
+ 1. Add a `"test"` property in the `package.json` file and give it a value of `"mocha"`
82
+ 1. Run `npm test` in your terminal from the root folder of the project
@@ -23,7 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  return (mod && mod.__esModule) ? mod : { "default": mod };
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.updateSenderView = exports.conversationRoute = void 0;
26
+ exports.editConversationMessage = exports.sendMessage = exports.updateSenderView = exports.conversationRoute = void 0;
27
27
  const fs_1 = __importDefault(require("fs"));
28
28
  const path_1 = __importDefault(require("path"));
29
29
  const util_1 = require("util");
@@ -38,6 +38,7 @@ const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg"));
38
38
  const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
39
39
  const ffprobe_static_1 = __importDefault(require("ffprobe-static"));
40
40
  const prompt_1 = require("../../prompt");
41
+ const mediaProcessing_1 = require("../../mediaProcessing");
41
42
  fluent_ffmpeg_1.default.setFfmpegPath(ffmpeg_static_1.default);
42
43
  fluent_ffmpeg_1.default.setFfprobePath(ffprobe_static_1.default.path);
43
44
  const conversationRoute = (_a) => {
@@ -128,25 +129,67 @@ const deleteConversationMessage = (req, res, { isClient }) => __awaiter(void 0,
128
129
  // - Se elimina la sugerencia
129
130
  // - Se elimina el mensaje programado
130
131
  // TODO: Comprobar que el cliente/entrenador puede editar/eliminar el mensaje (no se haya contestado ya y que no haya pasado el tiempo de cortesía)
132
+ /**
133
+ * Edita un mensaje de chat existente
134
+ *
135
+ * @param req - Request con params y body
136
+ * @param req.params.id - ID del mensaje a editar
137
+ * @param req.body.text - Nuevo texto del mensaje
138
+ * @param req.body.idWorkout - ID del workout a linkear (opcional, solo entrenadores). Si se envía null, se quita el workout linkeado
139
+ * @param res - Response
140
+ * @param params.isClient - Si el sender es cliente (true) o entrenador (false)
141
+ * @returns Promise<void> - Envía { status: 'ok' } en la respuesta
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * // Editar solo el texto
146
+ * PUT /chat/conversation/123
147
+ * { "text": "Texto corregido" }
148
+ *
149
+ * // Editar texto y agregar workout
150
+ * PUT /chat/conversation/123
151
+ * { "text": "Texto corregido", "idWorkout": "workout456" }
152
+ *
153
+ * // Quitar workout linkeado
154
+ * PUT /chat/conversation/123
155
+ * { "text": "Texto corregido", "idWorkout": null }
156
+ * ```
157
+ */
131
158
  const editConversationMessage = (req, res, { isClient }) => __awaiter(void 0, void 0, void 0, function* () {
132
159
  const { id } = req.params;
133
- const { text } = req.body;
160
+ const { text, idWorkout: idWorkoutBody } = req.body;
134
161
  if (!(yield canEditOrDeleteMessage({ idMessage: id, isClient, userid: req.session.userid })))
135
162
  return res.send({ status: 'ok' });
136
- const [message] = yield (0, index_1.query)('SELECT [ID CLIENTE] FROM [CHAT MESSAGE] WHERE [ID] = ?', [id]);
163
+ const [message] = yield (0, index_1.query)('SELECT [ID CLIENTE], [ID WORKOUT] FROM [CHAT MESSAGE] WHERE [ID] = ?', [id]);
137
164
  // Devuelve el texto en el otro idioma si el cliente no habla español y el idioma del cliente
138
165
  const { textSpanish, textPreferredLanguage } = yield getPreferredLanguageForChat({
139
166
  text,
140
167
  idCliente: message.idCliente,
141
168
  isClient,
142
169
  });
143
- yield (0, index_1.query)('UPDATE [CHAT MESSAGE] SET [TEXT] = ?, [TEXT PREFERRED LANGUAGE] = ?, [EDITADO] = TRUE WHERE [ID] = ?', [
170
+ // Linkeo de workout desde la edición (solo para entrenadores)
171
+ let idWorkout = message.idWorkout; // Mantener el workout existente por defecto
172
+ if (idWorkoutBody && !isClient) {
173
+ // Validar que el workout existe y pertenece al cliente
174
+ const [workout] = yield (0, index_1.query)('SELECT [ID] FROM [WORKOUT] WHERE [ID] = ? AND [ID CLIENTE] = ?', [idWorkoutBody, message.idCliente]);
175
+ if (workout) {
176
+ idWorkout = idWorkoutBody;
177
+ }
178
+ // Si el workout no existe o no pertenece al cliente, se mantiene el idWorkout actual (o null)
179
+ }
180
+ else if (idWorkoutBody === null && !isClient) {
181
+ // Permitir quitar el workout si se envía explícitamente null
182
+ idWorkout = null;
183
+ }
184
+ yield (0, index_1.query)('UPDATE [CHAT MESSAGE] SET [TEXT] = ?, [TEXT PREFERRED LANGUAGE] = ?, [ID WORKOUT] = ?, [EDITADO] = TRUE WHERE [ID] = ?', [
144
185
  textSpanish,
145
186
  textPreferredLanguage,
187
+ idWorkout,
146
188
  id,
147
189
  ]);
148
190
  res.send({ status: 'ok' });
149
191
  });
192
+ exports.editConversationMessage = editConversationMessage;
150
193
  const canEditOrDeleteMessage = ({ idMessage, isClient, userid }) => __awaiter(void 0, void 0, void 0, function* () {
151
194
  const [message] = yield (0, index_1.query)('SELECT [ID], "ID CLIENTE", "ID SENDER" FROM [CHAT MESSAGE] WHERE [ID] = ? AND (? = FALSE OR [ID CLIENTE] = ?)', [
152
195
  idMessage,
@@ -239,8 +282,34 @@ const getChatFile = ({ bucket, id, isClient, userid }) => __awaiter(void 0, void
239
282
  return null;
240
283
  }
241
284
  });
285
+ /**
286
+ * Envía un mensaje de chat entre entrenador y cliente
287
+ *
288
+ * @param req - Request con body conteniendo:
289
+ * @param req.body.text - Texto del mensaje
290
+ * @param req.body.idCliente - ID del cliente destinatario (solo para entrenadores)
291
+ * @param req.body.replyMessageId - ID del mensaje al que se responde (opcional)
292
+ * @param req.body.new - Objeto con datos de comentario intelligence (opcional, tipo 6)
293
+ * @param req.body.idWorkout - ID del workout a linkear en el mensaje (opcional, solo entrenadores)
294
+ * @param res - Response
295
+ * @param params.sendNotification - Función para enviar notificaciones push
296
+ * @param params.firebaseMessaging - Instancia de Firebase Messaging
297
+ * @param params.isClient - Si el sender es cliente (true) o entrenador (false)
298
+ * @returns Promise<void> - Envía { idMessage, text } en la respuesta
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * // Mensaje normal de entrenador
303
+ * POST /chat/conversation/send
304
+ * { "idCliente": "abc123", "text": "Hola!" }
305
+ *
306
+ * // Mensaje con workout linkeado
307
+ * POST /chat/conversation/send
308
+ * { "idCliente": "abc123", "text": "Para el entreno de series...", "idWorkout": "workout456" }
309
+ * ```
310
+ */
242
311
  const sendMessage = (req, res, { sendNotification, firebaseMessaging, isClient }) => __awaiter(void 0, void 0, void 0, function* () {
243
- const { replyMessageId, new: newMessage } = req.body;
312
+ const { replyMessageId, new: newMessage, idWorkout: idWorkoutBody } = req.body;
244
313
  let { text } = req.body;
245
314
  const { userid } = req.session;
246
315
  const idCliente = isClient ? req.session.userid : req.body.idCliente;
@@ -264,6 +333,14 @@ const sendMessage = (req, res, { sendNotification, firebaseMessaging, isClient }
264
333
  type = 6;
265
334
  idWorkout = newMessage.idWorkout;
266
335
  }
336
+ // ✅ Linkeo de workout desde el chat (cuando no es tipo intelligence)
337
+ // Solo para entrenadores (!isClient) - los clientes no pueden linkear workouts
338
+ if (!idWorkout && idWorkoutBody && !isClient) {
339
+ const [workout] = yield (0, index_1.query)('SELECT [ID] FROM [WORKOUT] WHERE [ID] = ? AND [ID CLIENTE] = ?', [idWorkoutBody, idCliente]);
340
+ if (workout) {
341
+ idWorkout = idWorkoutBody;
342
+ }
343
+ }
267
344
  const [message] = yield (0, index_1.query)('INSERT INTO [CHAT MESSAGE] ([ID CLIENTE], [ID SENDER], [TEXT], [TEXT PREFERRED LANGUAGE], [PREFERRED LANGUAGE], [REPLY MESSAGE ID], [ID WORKOUT], [TYPE]) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING [ID]', [isClient ? userid : idCliente, userid, textSpanish, textPreferredLanguage, preferredLanguage, replyMessageId, idWorkout, type]);
268
345
  res.send({ idMessage: message.id, text: textSpanish });
269
346
  if (!isClient) {
@@ -288,6 +365,7 @@ const sendMessage = (req, res, { sendNotification, firebaseMessaging, isClient }
288
365
  yield updateSenderView({ userid, idCliente, idMessage: message.id });
289
366
  }
290
367
  });
368
+ exports.sendMessage = sendMessage;
291
369
  const updateSenderView = ({ userid, idCliente, idMessage }) => __awaiter(void 0, void 0, void 0, function* () {
292
370
  const [cliente] = yield (0, index_1.query)('SELECT [ID ENTRENADOR PRINCIPAL], [ID ENTRENADOR SECUNDARIO] FROM [CLIENTE] WHERE [ID] = ?', [idCliente]);
293
371
  let idSenderView;
@@ -394,6 +472,10 @@ const sendFile = (req, res, { sendNotification, firebaseMessaging, isClient, buc
394
472
  yield bucket.file(`Chat/${file.id}`).save(file.data);
395
473
  }
396
474
  fs_1.default.unlinkSync(filePath);
475
+ // ✅ Procesar archivo multimedia en background (transcripción de audio / descripción de imagen)
476
+ if (req.file.mimetype.startsWith('audio/') || req.file.mimetype.startsWith('image/')) {
477
+ (0, mediaProcessing_1.processMediaFile)(idFile, fileData, req.file.mimetype);
478
+ }
397
479
  if (!isClient) {
398
480
  let textFile = 'Archivo adjunto';
399
481
  if (Number(type) === 4)
@@ -32,18 +32,18 @@ const getList = (req, res, { query, isClient }) => __awaiter(void 0, void 0, voi
32
32
  else {
33
33
  const { page } = req.query;
34
34
  const limit = 20;
35
- const list = yield query(`
36
- SELECT [C].[ID], [C].[NAME],
37
- [CM].[DATE], [CM].[TEXT],
38
- CASE WHEN [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE] THEN 0
39
- ELSE 1 END AS [READ]
40
- FROM [CLIENTE] AS [C]
41
- LEFT JOIN (
42
- SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
43
- FROM [CHAT MESSAGE]
44
- ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
45
- ORDER BY CASE WHEN [CM].[DATE] IS NULL THEN 1 ELSE 0 END, [CM].[DATE] DESC
46
- LIMIT ? OFFSET ?
35
+ const list = yield query(`
36
+ SELECT [C].[ID], [C].[NAME],
37
+ [CM].[DATE], [CM].[TEXT],
38
+ CASE WHEN [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE] THEN 0
39
+ ELSE 1 END AS [READ]
40
+ FROM [CLIENTE] AS [C]
41
+ LEFT JOIN (
42
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
43
+ FROM [CHAT MESSAGE]
44
+ ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
45
+ ORDER BY CASE WHEN [CM].[DATE] IS NULL THEN 1 ELSE 0 END, [CM].[DATE] DESC
46
+ LIMIT ? OFFSET ?
47
47
  `, [limit, limit * page]);
48
48
  res.send(list);
49
49
  }
@@ -16,11 +16,11 @@ const saveResponseTime = (idCliente) => __awaiter(void 0, void 0, void 0, functi
16
16
  let firstNotReadMessage;
17
17
  // eslint-disable-next-line no-constant-condition
18
18
  while (true) {
19
- const [lastMessage] = yield (0, index_1.query)(`
20
- SELECT * FROM [CHAT MESSAGE]
21
- WHERE [ID CLIENTE] = ? AND [ID] < ?
22
- ORDER BY [DATE] DESC
23
- LIMIT 1
19
+ const [lastMessage] = yield (0, index_1.query)(`
20
+ SELECT * FROM [CHAT MESSAGE]
21
+ WHERE [ID CLIENTE] = ? AND [ID] < ?
22
+ ORDER BY [DATE] DESC
23
+ LIMIT 1
24
24
  `, [idCliente, (lastNotReadMessage === null || lastNotReadMessage === void 0 ? void 0 : lastNotReadMessage.id) || 2147483646]);
25
25
  // Si no hay más mensajes || el mensaje no es del cliente || ya tiene tiempo de respuesta => salimos del bucle
26
26
  if (!lastMessage || (lastMessage.idSender && lastMessage.idCliente !== lastMessage.idSender) || lastMessage.tiempoRespuesta)
@@ -12,22 +12,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getCountNotificaciones = void 0;
13
13
  const getCountNotificaciones = ({ query, isClient, idClient }) => __awaiter(void 0, void 0, void 0, function* () {
14
14
  if (isClient) {
15
- const [{ chat }] = yield query(`
16
- SELECT COUNT([ID]) AS [CHAT]
17
- FROM [CHAT MESSAGE]
18
- WHERE [ID CLIENTE] = ? AND [READ] = FALSE AND [ID SENDER] != [ID CLIENTE]
15
+ const [{ chat }] = yield query(`
16
+ SELECT COUNT([ID]) AS [CHAT]
17
+ FROM [CHAT MESSAGE]
18
+ WHERE [ID CLIENTE] = ? AND [READ] = FALSE AND [ID SENDER] != [ID CLIENTE]
19
19
  `, [idClient]);
20
20
  return chat;
21
21
  }
22
22
  else {
23
- const [{ chat }] = yield query(`
24
- SELECT COUNT([C].[ID]) AS [CHAT]
25
- FROM [CLIENTE] AS [C]
26
- LEFT JOIN (
27
- SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
28
- FROM [CHAT MESSAGE]
29
- ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
30
- WHERE [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE]
23
+ const [{ chat }] = yield query(`
24
+ SELECT COUNT([C].[ID]) AS [CHAT]
25
+ FROM [CLIENTE] AS [C]
26
+ LEFT JOIN (
27
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY [ID CLIENTE] ORDER BY [DATE] DESC) AS [ROW]
28
+ FROM [CHAT MESSAGE]
29
+ ) AS [CM] ON [CM].[ID CLIENTE] = [C].[ID] AND [CM].[ROW] = 1
30
+ WHERE [CM].[READ] = FALSE AND [CM].[ID SENDER] = [CM].[ID CLIENTE]
31
31
  `);
32
32
  return chat;
33
33
  }
package/lib/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.updateSenderView = exports.sendPrompt = exports.saveResponseTime = exports.getPlanificacionPrueba7dias = exports.sendWorkoutToWatch = exports.getDefaultWorkoutImage = exports.generateShareMap = exports.reduceSizeImage = exports.getLetter = exports.getNumberByLetter = exports.appendSheet = exports.writeSheet = exports.findCellByValue = exports.readSheet = exports.NOTION_DATABASES_ID = exports.notionEditPage = exports.notionAddPage = exports.notionGetDatabase = exports.notionGetUsers = exports.getCountNotificaciones = exports.chatExposed = exports.chatApi = exports.chat = exports.getExerciseTranslatedDescription = exports.useTranslation = exports.LANGUAGES = exports.translate = exports.CHANNEL_SLACK = exports.notifySlack = exports.fetchIA = exports.err = exports.sendMail = exports.pool = exports.toPgArray = exports.batchQuery = exports.longRunningQuery = exports.queryWithClient = exports.query = exports.sleep = exports.sendNotification = void 0;
3
+ exports.reprocessRecentMedia = exports.processMediaFile = exports.describeImage = exports.transcribeAudio = exports.updateSenderView = exports.sendPrompt = exports.saveResponseTime = exports.getPlanificacionPrueba7dias = exports.sendWorkoutToWatch = exports.getDefaultWorkoutImage = exports.generateShareMap = exports.reduceSizeImage = exports.getLetter = exports.getNumberByLetter = exports.appendSheet = exports.writeSheet = exports.findCellByValue = exports.readSheet = exports.NOTION_DATABASES_ID = exports.notionEditPage = exports.notionAddPage = exports.notionGetDatabase = exports.notionGetUsers = exports.getCountNotificaciones = exports.chatExposed = exports.chatApi = exports.chat = exports.getExerciseTranslatedDescription = exports.useTranslation = exports.LANGUAGES = exports.translate = exports.CHANNEL_SLACK = exports.notifySlack = exports.fetchIA = exports.err = exports.sendMail = exports.pool = exports.toPgArray = exports.batchQuery = exports.longRunningQuery = exports.queryWithClient = exports.query = exports.sleep = exports.sendNotification = void 0;
4
4
  const sendNotification_1 = require("./sendNotification");
5
5
  Object.defineProperty(exports, "sendNotification", { enumerable: true, get: function () { return sendNotification_1.sendNotification; } });
6
6
  const sleep_1 = require("./sleep");
@@ -60,3 +60,8 @@ const prompt_1 = require("./prompt");
60
60
  Object.defineProperty(exports, "sendPrompt", { enumerable: true, get: function () { return prompt_1.sendPrompt; } });
61
61
  const conversation_1 = require("./chat/api/conversation");
62
62
  Object.defineProperty(exports, "updateSenderView", { enumerable: true, get: function () { return conversation_1.updateSenderView; } });
63
+ const mediaProcessing_1 = require("./mediaProcessing");
64
+ Object.defineProperty(exports, "transcribeAudio", { enumerable: true, get: function () { return mediaProcessing_1.transcribeAudio; } });
65
+ Object.defineProperty(exports, "describeImage", { enumerable: true, get: function () { return mediaProcessing_1.describeImage; } });
66
+ Object.defineProperty(exports, "processMediaFile", { enumerable: true, get: function () { return mediaProcessing_1.processMediaFile; } });
67
+ Object.defineProperty(exports, "reprocessRecentMedia", { enumerable: true, get: function () { return mediaProcessing_1.reprocessRecentMedia; } });
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.reprocessRecentMedia = exports.processMediaFile = exports.describeImage = exports.transcribeAudio = void 0;
13
+ const vertexai_1 = require("@google-cloud/vertexai");
14
+ const index_1 = require("../db/index");
15
+ const index_2 = require("../err/index");
16
+ // ✅ PROMPTS PARA PROCESAMIENTO DE ARCHIVOS MULTIMEDIA
17
+ const AUDIO_TRANSCRIPTION_PROMPT = `Transcribe el audio de forma literal y completa en español.
18
+ Si el audio está en otro idioma, tradúcelo al español.
19
+ Devuelve ÚNICAMENTE la transcripción, sin comentarios adicionales.
20
+ Si no puedes entender el audio o está vacío, responde: "[Audio no reconocible]"`;
21
+ const IMAGE_DESCRIPTION_PROMPT = `Analiza esta imagen en el contexto de una aplicación de entrenamiento deportivo (running, ciclismo, natación, etc).
22
+
23
+ Identifica y describe:
24
+ 1. **Tipo de imagen**: captura de Garmin/Strava/reloj deportivo, foto de lesión/dolor, gráfica de entrenamiento, selfie deportivo, foto de equipamiento, etc.
25
+ 2. **Información relevante**: si es una captura de datos, extrae los valores importantes (ritmo, distancia, frecuencia cardíaca, zonas, etc.)
26
+ 3. **Contexto útil para el entrenador**: cualquier información que ayude a entender el estado del atleta o su entrenamiento.
27
+
28
+ Devuelve una descripción concisa y útil en español (máximo 300 palabras).
29
+ Si la imagen no está relacionada con deporte/fitness, descríbela brevemente.`;
30
+ /**
31
+ * Transcribe un archivo de audio usando Gemini (Vertex AI)
32
+ *
33
+ * @param fileBuffer - Buffer del archivo de audio
34
+ * @param mimetype - Tipo MIME del archivo (ej: 'audio/mpeg', 'audio/wav')
35
+ * @returns Promise<string> - Transcripción del audio
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const transcription = await transcribeAudio(audioBuffer, 'audio/mpeg');
40
+ * // Resultado: "Hola, quería comentarte que hoy me duele un poco la rodilla..."
41
+ * ```
42
+ */
43
+ const transcribeAudio = (fileBuffer, mimetype) => __awaiter(void 0, void 0, void 0, function* () {
44
+ var _a, _b, _c, _d;
45
+ try {
46
+ const vertex_ai = new vertexai_1.VertexAI({
47
+ project: process.env.PROJECT_ID,
48
+ location: 'europe-west8',
49
+ });
50
+ // ⭐ Usamos gemini-2.5-flash para balance entre velocidad y precisión
51
+ const generativeModel = vertex_ai.preview.getGenerativeModel({
52
+ model: 'gemini-2.5-flash',
53
+ generationConfig: {
54
+ temperature: 0.1, // Baja temperatura para transcripción precisa
55
+ topP: 0.8,
56
+ topK: 20,
57
+ },
58
+ });
59
+ const base64Audio = fileBuffer.toString('base64');
60
+ // @ts-ignore - Vertex AI types son complejos para contenido multimodal
61
+ const resp = yield generativeModel.generateContent({
62
+ contents: [
63
+ {
64
+ role: 'user',
65
+ parts: [
66
+ {
67
+ inlineData: {
68
+ mimeType: mimetype,
69
+ data: base64Audio,
70
+ },
71
+ },
72
+ {
73
+ text: AUDIO_TRANSCRIPTION_PROMPT,
74
+ },
75
+ ],
76
+ },
77
+ ],
78
+ });
79
+ const candidate = (_a = resp.response.candidates) === null || _a === void 0 ? void 0 : _a[0];
80
+ if (!candidate)
81
+ return '[Error al procesar audio]';
82
+ const parts = (_b = candidate.content) === null || _b === void 0 ? void 0 : _b.parts;
83
+ if (!parts || parts.length === 0)
84
+ return '[Error al procesar audio]';
85
+ return ((_d = (_c = parts[0]) === null || _c === void 0 ? void 0 : _c.text) === null || _d === void 0 ? void 0 : _d.trim()) || '[Audio vacío]';
86
+ }
87
+ catch (error) {
88
+ (0, index_2.err)(null, null, error, null);
89
+ return '[Error al transcribir audio]';
90
+ }
91
+ });
92
+ exports.transcribeAudio = transcribeAudio;
93
+ /**
94
+ * Genera una descripción de una imagen usando Gemini (Vertex AI)
95
+ * Contextualizada para una aplicación de fitness/entrenamiento
96
+ *
97
+ * @param fileBuffer - Buffer del archivo de imagen
98
+ * @param mimetype - Tipo MIME del archivo (ej: 'image/jpeg', 'image/png')
99
+ * @returns Promise<string> - Descripción de la imagen
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const description = await describeImage(imageBuffer, 'image/jpeg');
104
+ * // Resultado: "Captura de Garmin Connect mostrando un entrenamiento de 10km..."
105
+ * ```
106
+ */
107
+ const describeImage = (fileBuffer, mimetype) => __awaiter(void 0, void 0, void 0, function* () {
108
+ var _e, _f, _g, _h;
109
+ try {
110
+ const vertex_ai = new vertexai_1.VertexAI({
111
+ project: process.env.PROJECT_ID,
112
+ location: 'europe-west8',
113
+ });
114
+ // ⭐ Usamos gemini-2.5-flash para balance entre velocidad y precisión
115
+ const generativeModel = vertex_ai.preview.getGenerativeModel({
116
+ model: 'gemini-2.5-flash',
117
+ generationConfig: {
118
+ temperature: 0.3,
119
+ topP: 0.8,
120
+ topK: 20,
121
+ },
122
+ });
123
+ const base64Image = fileBuffer.toString('base64');
124
+ // @ts-ignore - Vertex AI types son complejos para contenido multimodal
125
+ const resp = yield generativeModel.generateContent({
126
+ contents: [
127
+ {
128
+ role: 'user',
129
+ parts: [
130
+ {
131
+ inlineData: {
132
+ mimeType: mimetype,
133
+ data: base64Image,
134
+ },
135
+ },
136
+ {
137
+ text: IMAGE_DESCRIPTION_PROMPT,
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ });
143
+ const candidate = (_e = resp.response.candidates) === null || _e === void 0 ? void 0 : _e[0];
144
+ if (!candidate)
145
+ return '[Error al procesar imagen]';
146
+ const parts = (_f = candidate.content) === null || _f === void 0 ? void 0 : _f.parts;
147
+ if (!parts || parts.length === 0)
148
+ return '[Error al procesar imagen]';
149
+ return ((_h = (_g = parts[0]) === null || _g === void 0 ? void 0 : _g.text) === null || _h === void 0 ? void 0 : _h.trim()) || '[Imagen no analizable]';
150
+ }
151
+ catch (error) {
152
+ (0, index_2.err)(null, null, error, null);
153
+ return '[Error al describir imagen]';
154
+ }
155
+ });
156
+ exports.describeImage = describeImage;
157
+ /**
158
+ * Procesa un archivo multimedia (audio o imagen) y guarda el resultado en la base de datos
159
+ * Esta función se ejecuta de forma asíncrona después de guardar el mensaje
160
+ *
161
+ * @param idMessage - ID del mensaje en CHAT MESSAGE
162
+ * @param fileBuffer - Buffer del archivo
163
+ * @param mimetype - Tipo MIME del archivo
164
+ * @returns Promise<void>
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * // No usar await para ejecutar en background
169
+ * processMediaFile(123, fileBuffer, 'audio/mpeg');
170
+ * ```
171
+ */
172
+ const processMediaFile = (idMessage, fileBuffer, mimetype) => __awaiter(void 0, void 0, void 0, function* () {
173
+ try {
174
+ let fileText;
175
+ if (mimetype.startsWith('audio/')) {
176
+ fileText = yield transcribeAudio(fileBuffer, mimetype);
177
+ }
178
+ else if (mimetype.startsWith('image/')) {
179
+ fileText = yield describeImage(fileBuffer, mimetype);
180
+ }
181
+ else {
182
+ // ❌ Tipo de archivo no soportado para procesamiento
183
+ return;
184
+ }
185
+ // ✅ Guardar el resultado en la base de datos
186
+ yield (0, index_1.query)('UPDATE [CHAT MESSAGE] SET [FILE TEXT] = ? WHERE [ID] = ?', [fileText, idMessage]);
187
+ }
188
+ catch (error) {
189
+ (0, index_2.err)(null, null, error, null);
190
+ }
191
+ });
192
+ exports.processMediaFile = processMediaFile;
193
+ /**
194
+ * Reprocesa mensajes multimedia recientes (últimos 2 días) que no tienen FILE TEXT
195
+ * Útil para procesar mensajes enviados antes del deploy de esta funcionalidad
196
+ *
197
+ * @param bucket - Instancia del bucket de Google Cloud Storage
198
+ * @returns Promise<{ processed: number; errors: number }> - Estadísticas del reprocesamiento
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * const stats = await reprocessRecentMedia(bucket);
203
+ * // Resultado: { processed: 5, errors: 1 }
204
+ * ```
205
+ */
206
+ const reprocessRecentMedia = (bucket) => __awaiter(void 0, void 0, void 0, function* () {
207
+ let processed = 0;
208
+ let errors = 0;
209
+ try {
210
+ // ✅ Buscar mensajes de los últimos 2 días con MIMETYPE de audio/imagen y sin FILE TEXT
211
+ const messages = yield (0, index_1.query)(`SELECT "ID", "MIMETYPE" FROM "CHAT MESSAGE"
212
+ WHERE "DATE" >= NOW() - INTERVAL '2 days'
213
+ AND "FILE TEXT" IS NULL
214
+ AND ("MIMETYPE" LIKE 'audio/%' OR "MIMETYPE" LIKE 'image/%')
215
+ ORDER BY "DATE" DESC`, []);
216
+ // eslint-disable-next-line no-console
217
+ console.log(`[reprocessRecentMedia] Encontrados ${messages.length} mensajes para procesar`);
218
+ for (const message of messages) {
219
+ try {
220
+ // ⭐ Descargar archivo de Storage
221
+ const [fileExists] = yield bucket.file(`Chat/${message.id}`).exists();
222
+ if (!fileExists) {
223
+ // eslint-disable-next-line no-console
224
+ console.log(`[reprocessRecentMedia] Archivo no encontrado: Chat/${message.id}`);
225
+ errors++;
226
+ continue;
227
+ }
228
+ const [fileBuffer] = yield bucket.file(`Chat/${message.id}`).download();
229
+ // ⭐ Procesar según tipo
230
+ let fileText;
231
+ if (message.mimetype.startsWith('audio/')) {
232
+ fileText = yield transcribeAudio(fileBuffer, message.mimetype);
233
+ }
234
+ else {
235
+ fileText = yield describeImage(fileBuffer, message.mimetype);
236
+ }
237
+ // ⭐ Guardar en BD
238
+ yield (0, index_1.query)('UPDATE [CHAT MESSAGE] SET [FILE TEXT] = ? WHERE [ID] = ?', [fileText, message.id]);
239
+ // eslint-disable-next-line no-console
240
+ console.log(`[reprocessRecentMedia] ✅ Procesado mensaje ${message.id}: ${fileText.substring(0, 50)}...`);
241
+ processed++;
242
+ }
243
+ catch (msgError) {
244
+ // eslint-disable-next-line no-console
245
+ console.log(`[reprocessRecentMedia] ❌ Error procesando mensaje ${message.id}:`, msgError);
246
+ errors++;
247
+ }
248
+ }
249
+ }
250
+ catch (error) {
251
+ (0, index_2.err)(null, null, error, null);
252
+ }
253
+ return { processed, errors };
254
+ });
255
+ exports.reprocessRecentMedia = reprocessRecentMedia;
@@ -45,72 +45,72 @@ const sendMail = ({ subject, title, body, to, link, attachments, bcc }) => __awa
45
45
  });
46
46
  exports.sendMail = sendMail;
47
47
  function getBodyHTML(title, body, link) {
48
- return `
49
- <html lang="es">
50
- <head>
51
- <meta charset="UTF-8">
52
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
53
- <title>Correo con estilo</title>
54
- <style>
55
- /* Estilos generales */
56
- body {
57
- margin: 0;
58
- padding: 0;
59
- }
60
-
61
- /* Estilos específicos del botón */
62
- .button {
63
- display: inline-block;
64
- padding: 12px 26px;
65
- font-size: 17px;
66
- text-align: center;
67
- text-decoration: none;
68
- background-color: #ea5b1b;
69
- color: #ffffff !important;
70
- margin: 40px 0 20px 0;
71
- font-family: 'Sofia Sans', 'Roboto', sans-serif;
72
- font-weight: bold;
73
- border-radius: 4px;
74
- border: 2px solid #ea5b1b;
75
- }
76
- </style>
77
- </head>
78
- <body style="background: #f0f0f0; padding: 0 0 40px 0">
79
- <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0">
80
- <tr>
81
- <td align="center" style="padding:0;">
82
- <table role="presentation" style="width:100%;border-collapse:collapse;border-spacing:0;text-align:left;">
83
- <tr>
84
- <td align="center" style="padding:5px 0 0 0;background:#ea5b1b;">
85
- <img src="cid:logo" alt="" width="220" style="height:auto;display:block;">
86
- </td>
87
- </tr>
88
- <tr>
89
- <td style="padding:20px 24px 0px 24px; max-width: 600px" align="center">
90
- <h1 style="font-size:22px;text-align:center;margin:16px 0 6px 0;font-family:'Sofia Sans', 'Roboto', sans-serif;font-style: italic">
91
- ${title || ''}
92
- </h1>
93
- <p style="text-align:left;margin:0 0 12px 0;font-size:15px;line-height:24px;font-family:'Sofia Sans', 'Roboto', sans-serif;">
94
- ${body.split('\n').join('<br>')}
95
- </p>
48
+ return `
49
+ <html lang="es">
50
+ <head>
51
+ <meta charset="UTF-8">
52
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
53
+ <title>Correo con estilo</title>
54
+ <style>
55
+ /* Estilos generales */
56
+ body {
57
+ margin: 0;
58
+ padding: 0;
59
+ }
60
+
61
+ /* Estilos específicos del botón */
62
+ .button {
63
+ display: inline-block;
64
+ padding: 12px 26px;
65
+ font-size: 17px;
66
+ text-align: center;
67
+ text-decoration: none;
68
+ background-color: #ea5b1b;
69
+ color: #ffffff !important;
70
+ margin: 40px 0 20px 0;
71
+ font-family: 'Sofia Sans', 'Roboto', sans-serif;
72
+ font-weight: bold;
73
+ border-radius: 4px;
74
+ border: 2px solid #ea5b1b;
75
+ }
76
+ </style>
77
+ </head>
78
+ <body style="background: #f0f0f0; padding: 0 0 40px 0">
79
+ <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0">
80
+ <tr>
81
+ <td align="center" style="padding:0;">
82
+ <table role="presentation" style="width:100%;border-collapse:collapse;border-spacing:0;text-align:left;">
83
+ <tr>
84
+ <td align="center" style="padding:5px 0 0 0;background:#ea5b1b;">
85
+ <img src="cid:logo" alt="" width="220" style="height:auto;display:block;">
86
+ </td>
87
+ </tr>
88
+ <tr>
89
+ <td style="padding:20px 24px 0px 24px; max-width: 600px" align="center">
90
+ <h1 style="font-size:22px;text-align:center;margin:16px 0 6px 0;font-family:'Sofia Sans', 'Roboto', sans-serif;font-style: italic">
91
+ ${title || ''}
92
+ </h1>
93
+ <p style="text-align:left;margin:0 0 12px 0;font-size:15px;line-height:24px;font-family:'Sofia Sans', 'Roboto', sans-serif;">
94
+ ${body.split('\n').join('<br>')}
95
+ </p>
96
96
  ${link
97
- ? `
98
- <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0;">
99
- <tr>
100
- <td align="center" style="padding:0;">
101
- <a href="${link.external ? '' : process.env.FRONTEND_URL}${link.url}" class="button" style="color:white">${link.text}</a>
102
- </td>
103
- </tr>
104
- </table>
97
+ ? `
98
+ <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0;">
99
+ <tr>
100
+ <td align="center" style="padding:0;">
101
+ <a href="${link.external ? '' : process.env.FRONTEND_URL}${link.url}" class="button" style="color:white">${link.text}</a>
102
+ </td>
103
+ </tr>
104
+ </table>
105
105
  `
106
- : ''}
107
- </td>
108
- </tr>
109
- </table>
110
- </td>
111
- </tr>
112
- </table>
113
- </body>
114
- </html>
106
+ : ''}
107
+ </td>
108
+ </tr>
109
+ </table>
110
+ </td>
111
+ </tr>
112
+ </table>
113
+ </body>
114
+ </html>
115
115
  `;
116
116
  }
@@ -1,8 +1,62 @@
1
1
  declare const conversationRoute: ({ router, ...params }: any) => void;
2
+ /**
3
+ * Edita un mensaje de chat existente
4
+ *
5
+ * @param req - Request con params y body
6
+ * @param req.params.id - ID del mensaje a editar
7
+ * @param req.body.text - Nuevo texto del mensaje
8
+ * @param req.body.idWorkout - ID del workout a linkear (opcional, solo entrenadores). Si se envía null, se quita el workout linkeado
9
+ * @param res - Response
10
+ * @param params.isClient - Si el sender es cliente (true) o entrenador (false)
11
+ * @returns Promise<void> - Envía { status: 'ok' } en la respuesta
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Editar solo el texto
16
+ * PUT /chat/conversation/123
17
+ * { "text": "Texto corregido" }
18
+ *
19
+ * // Editar texto y agregar workout
20
+ * PUT /chat/conversation/123
21
+ * { "text": "Texto corregido", "idWorkout": "workout456" }
22
+ *
23
+ * // Quitar workout linkeado
24
+ * PUT /chat/conversation/123
25
+ * { "text": "Texto corregido", "idWorkout": null }
26
+ * ```
27
+ */
28
+ declare const editConversationMessage: (req: any, res: any, { isClient }: any) => Promise<any>;
29
+ /**
30
+ * Envía un mensaje de chat entre entrenador y cliente
31
+ *
32
+ * @param req - Request con body conteniendo:
33
+ * @param req.body.text - Texto del mensaje
34
+ * @param req.body.idCliente - ID del cliente destinatario (solo para entrenadores)
35
+ * @param req.body.replyMessageId - ID del mensaje al que se responde (opcional)
36
+ * @param req.body.new - Objeto con datos de comentario intelligence (opcional, tipo 6)
37
+ * @param req.body.idWorkout - ID del workout a linkear en el mensaje (opcional, solo entrenadores)
38
+ * @param res - Response
39
+ * @param params.sendNotification - Función para enviar notificaciones push
40
+ * @param params.firebaseMessaging - Instancia de Firebase Messaging
41
+ * @param params.isClient - Si el sender es cliente (true) o entrenador (false)
42
+ * @returns Promise<void> - Envía { idMessage, text } en la respuesta
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // Mensaje normal de entrenador
47
+ * POST /chat/conversation/send
48
+ * { "idCliente": "abc123", "text": "Hola!" }
49
+ *
50
+ * // Mensaje con workout linkeado
51
+ * POST /chat/conversation/send
52
+ * { "idCliente": "abc123", "text": "Para el entreno de series...", "idWorkout": "workout456" }
53
+ * ```
54
+ */
55
+ declare const sendMessage: (req: any, res: any, { sendNotification, firebaseMessaging, isClient }: any) => Promise<void>;
2
56
  declare const updateSenderView: ({ userid, idCliente, idMessage }: {
3
57
  userid: any;
4
58
  idCliente: any;
5
59
  idMessage: any;
6
60
  }) => Promise<void>;
7
- export { conversationRoute, updateSenderView };
61
+ export { conversationRoute, updateSenderView, sendMessage, editConversationMessage };
8
62
  //# sourceMappingURL=conversation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../../../src/chat/api/conversation.ts"],"names":[],"mappings":"AAkBA,QAAA,MAAM,iBAAiB,0BAA2B,GAAG,SA0BpD,CAAC;AA8PF,QAAA,MAAM,gBAAgB;;;;mBAkBrB,CAAC;AAsRF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../../../src/chat/api/conversation.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,iBAAiB,0BAA2B,GAAG,SA0BpD,CAAC;AA0EF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,uBAAuB,qCAAkC,GAAG,iBAqCjE,CAAC;AAmGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,WAAW,0EAAuE,GAAG,kBAkE1F,CAAC;AAEF,QAAA,MAAM,gBAAgB;;;;mBAqBrB,CAAC;AA8RF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC"}
@@ -17,5 +17,6 @@ import { getPlanificacionPrueba7dias } from './workout/planificacionPrueba7dias'
17
17
  import { saveResponseTime } from './chat/saveResponseTime';
18
18
  import { sendPrompt } from './prompt';
19
19
  import { updateSenderView } from './chat/api/conversation';
20
- export { sendNotification, sleep, query, queryWithClient, longRunningQuery, batchQuery, toPgArray, pool, sendMail, err, fetchIA, notifySlack, CHANNEL_SLACK, translate, LANGUAGES, useTranslation, getExerciseTranslatedDescription, chat, chatApi, chatExposed, getCountNotificaciones, notionGetUsers, notionGetDatabase, notionAddPage, notionEditPage, NOTION_DATABASES_ID, readSheet, findCellByValue, writeSheet, appendSheet, getNumberByLetter, getLetter, reduceSizeImage, generateShareMap, getDefaultWorkoutImage, sendWorkoutToWatch, getPlanificacionPrueba7dias, saveResponseTime, sendPrompt, updateSenderView, };
20
+ import { transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia } from './mediaProcessing';
21
+ export { sendNotification, sleep, query, queryWithClient, longRunningQuery, batchQuery, toPgArray, pool, sendMail, err, fetchIA, notifySlack, CHANNEL_SLACK, translate, LANGUAGES, useTranslation, getExerciseTranslatedDescription, chat, chatApi, chatExposed, getCountNotificaciones, notionGetUsers, notionGetDatabase, notionAddPage, notionEditPage, NOTION_DATABASES_ID, readSheet, findCellByValue, writeSheet, appendSheet, getNumberByLetter, getLetter, reduceSizeImage, generateShareMap, getDefaultWorkoutImage, sendWorkoutToWatch, getPlanificacionPrueba7dias, saveResponseTime, sendPrompt, updateSenderView, transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia, };
21
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,gCAAgC,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,KAAK,EACL,KAAK,EACL,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,GAAG,EACH,OAAO,EACP,WAAW,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,cAAc,EACd,gCAAgC,EAChC,IAAI,EACJ,OAAO,EACP,WAAW,EACX,sBAAsB,EACtB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,EAC3B,gBAAgB,EAChB,UAAU,EACV,gBAAgB,GACjB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,gCAAgC,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE3G,OAAO,EACL,gBAAgB,EAChB,KAAK,EACL,KAAK,EACL,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,GAAG,EACH,OAAO,EACP,WAAW,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,cAAc,EACd,gCAAgC,EAChC,IAAI,EACJ,OAAO,EACP,WAAW,EACX,sBAAsB,EACtB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,EAC3B,gBAAgB,EAChB,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,oBAAoB,GACrB,CAAC"}
@@ -0,0 +1,65 @@
1
+ /// <reference types="node" />
2
+ /**
3
+ * Transcribe un archivo de audio usando Gemini (Vertex AI)
4
+ *
5
+ * @param fileBuffer - Buffer del archivo de audio
6
+ * @param mimetype - Tipo MIME del archivo (ej: 'audio/mpeg', 'audio/wav')
7
+ * @returns Promise<string> - Transcripción del audio
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const transcription = await transcribeAudio(audioBuffer, 'audio/mpeg');
12
+ * // Resultado: "Hola, quería comentarte que hoy me duele un poco la rodilla..."
13
+ * ```
14
+ */
15
+ declare const transcribeAudio: (fileBuffer: Buffer, mimetype: string) => Promise<string>;
16
+ /**
17
+ * Genera una descripción de una imagen usando Gemini (Vertex AI)
18
+ * Contextualizada para una aplicación de fitness/entrenamiento
19
+ *
20
+ * @param fileBuffer - Buffer del archivo de imagen
21
+ * @param mimetype - Tipo MIME del archivo (ej: 'image/jpeg', 'image/png')
22
+ * @returns Promise<string> - Descripción de la imagen
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const description = await describeImage(imageBuffer, 'image/jpeg');
27
+ * // Resultado: "Captura de Garmin Connect mostrando un entrenamiento de 10km..."
28
+ * ```
29
+ */
30
+ declare const describeImage: (fileBuffer: Buffer, mimetype: string) => Promise<string>;
31
+ /**
32
+ * Procesa un archivo multimedia (audio o imagen) y guarda el resultado en la base de datos
33
+ * Esta función se ejecuta de forma asíncrona después de guardar el mensaje
34
+ *
35
+ * @param idMessage - ID del mensaje en CHAT MESSAGE
36
+ * @param fileBuffer - Buffer del archivo
37
+ * @param mimetype - Tipo MIME del archivo
38
+ * @returns Promise<void>
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // No usar await para ejecutar en background
43
+ * processMediaFile(123, fileBuffer, 'audio/mpeg');
44
+ * ```
45
+ */
46
+ declare const processMediaFile: (idMessage: number, fileBuffer: Buffer, mimetype: string) => Promise<void>;
47
+ /**
48
+ * Reprocesa mensajes multimedia recientes (últimos 2 días) que no tienen FILE TEXT
49
+ * Útil para procesar mensajes enviados antes del deploy de esta funcionalidad
50
+ *
51
+ * @param bucket - Instancia del bucket de Google Cloud Storage
52
+ * @returns Promise<{ processed: number; errors: number }> - Estadísticas del reprocesamiento
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const stats = await reprocessRecentMedia(bucket);
57
+ * // Resultado: { processed: 5, errors: 1 }
58
+ * ```
59
+ */
60
+ declare const reprocessRecentMedia: (bucket: any) => Promise<{
61
+ processed: number;
62
+ errors: number;
63
+ }>;
64
+ export { transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia };
65
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mediaProcessing/index.ts"],"names":[],"mappings":";AAqBA;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,eAAe,eAAsB,MAAM,YAAY,MAAM,KAAG,QAAQ,MAAM,CAkDnF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,aAAa,eAAsB,MAAM,YAAY,MAAM,KAAG,QAAQ,MAAM,CAkDjF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,QAAA,MAAM,gBAAgB,cAAqB,MAAM,cAAc,MAAM,YAAY,MAAM,KAAG,QAAQ,IAAI,CAkBrG,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,oBAAoB,WAAkB,GAAG;eAAwB,MAAM;YAAU,MAAM;EAwD5F,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,79 +1,81 @@
1
- {
2
- "name": "@runnerpro/backend",
3
- "version": "1.17.1",
4
- "description": "A collection of common backend functions",
5
- "exports": {
6
- ".": "./lib/cjs/index.js"
7
- },
8
- "types": "./lib/cjs/types/index.d.ts",
9
- "main": "./lib/cjs/index.js",
10
- "files": [
11
- "lib/**/*"
12
- ],
13
- "scripts": {
14
- "clean": "del-cli ./lib",
15
- "build": "npm run clean && tsc -p ./configs/tsconfig.cjs.json",
16
- "deploy": "npm run build && npm publish",
17
- "semantic-release": "semantic-release",
18
- "lint": "eslint --ext .ts --ignore-path .gitignore .",
19
- "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
20
- "prepare": "husky",
21
- "test": "jest"
22
- },
23
- "release": {
24
- "branches": [
25
- "main"
26
- ]
27
- },
28
- "publishConfig": {
29
- "access": "public"
30
- },
31
- "repository": {
32
- "type": "git",
33
- "url": "https://gitlab.com/runner-pro/runnerpro-backend.git"
34
- },
35
- "author": "Runner Pro",
36
- "license": "MIT",
37
- "devDependencies": {
38
- "@eslint/js": "^9.6.0",
39
- "@types/eslint__js": "^8.42.3",
40
- "@types/nodemailer": "^6.4.15",
41
- "@types/pg": "^8.11.3",
42
- "del-cli": "5.1.0",
43
- "eslint": "^8.57.0",
44
- "eslint-config-prettier": "^9.1.0",
45
- "eslint-plugin-exception-handling": "^1.0.2",
46
- "eslint-plugin-sonarjs": "^1.0.3",
47
- "husky": "^9.0.11",
48
- "jest": "^29.7.0",
49
- "semantic-release": "23.0.2",
50
- "ts-node": "10.9.2",
51
- "typescript": "5.3.3",
52
- "typescript-eslint": "^7.15.0"
53
- },
54
- "peerDependencies": {
55
- "@napi-rs/canvas": "^0.1.53",
56
- "@runnerpro/common": "^1.5.10",
57
- "axios": "^1.6.7",
58
- "image-size": "^1.0.2",
59
- "jimp": "^0.22.10",
60
- "nodemailer": "6.9.9",
61
- "pg": "8.11.3",
62
- "uuidv4": "^6.2.13"
63
- },
64
- "dependencies": {
65
- "@google-cloud/storage": "^7.11.3",
66
- "@google-cloud/translate": "^8.3.0",
67
- "@google-cloud/vertexai": "^1.4.0",
68
- "@notionhq/client": "^2.2.15",
69
- "exifr": "^7.1.3",
70
- "ffmpeg-static": "^5.2.0",
71
- "ffprobe-static": "^3.1.0",
72
- "firebase-admin": "^11.10.1",
73
- "fluent-ffmpeg": "^2.1.3",
74
- "googleapis": "^144.0.0",
75
- "multer": "^1.4.5-lts.1",
76
- "oauth-signature": "1.5.0",
77
- "socket.io": "^4.7.2"
78
- }
79
- }
1
+ {
2
+ "name": "@runnerpro/backend",
3
+ "version": "1.17.3",
4
+ "description": "A collection of common backend functions",
5
+ "exports": {
6
+ ".": "./lib/cjs/index.js"
7
+ },
8
+ "types": "./lib/cjs/types/index.d.ts",
9
+ "main": "./lib/cjs/index.js",
10
+ "files": [
11
+ "lib/**/*"
12
+ ],
13
+ "scripts": {
14
+ "clean": "del-cli ./lib",
15
+ "build": "npm run clean && tsc -p ./configs/tsconfig.cjs.json",
16
+ "deploy": "npm run build && npm publish",
17
+ "semantic-release": "semantic-release",
18
+ "lint": "eslint --ext .ts --ignore-path .gitignore .",
19
+ "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
20
+ "prepare": "husky",
21
+ "test": "jest"
22
+ },
23
+ "release": {
24
+ "branches": [
25
+ "main"
26
+ ]
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://gitlab.com/runner-pro/runnerpro-backend.git"
34
+ },
35
+ "author": "Runner Pro",
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@eslint/js": "^9.6.0",
39
+ "@types/eslint__js": "^8.42.3",
40
+ "@types/jest": "^30.0.0",
41
+ "@types/nodemailer": "^6.4.15",
42
+ "@types/pg": "^8.11.3",
43
+ "del-cli": "5.1.0",
44
+ "eslint": "^8.57.0",
45
+ "eslint-config-prettier": "^9.1.0",
46
+ "eslint-plugin-exception-handling": "^1.0.2",
47
+ "eslint-plugin-sonarjs": "^1.0.3",
48
+ "husky": "^9.0.11",
49
+ "jest": "^29.7.0",
50
+ "semantic-release": "23.0.2",
51
+ "ts-jest": "^29.4.6",
52
+ "ts-node": "10.9.2",
53
+ "typescript": "5.3.3",
54
+ "typescript-eslint": "^7.15.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@napi-rs/canvas": "^0.1.53",
58
+ "@runnerpro/common": "^1.5.10",
59
+ "axios": "^1.6.7",
60
+ "image-size": "^1.0.2",
61
+ "jimp": "^0.22.10",
62
+ "nodemailer": "6.9.9",
63
+ "pg": "8.11.3",
64
+ "uuidv4": "^6.2.13"
65
+ },
66
+ "dependencies": {
67
+ "@google-cloud/storage": "^7.11.3",
68
+ "@google-cloud/translate": "^8.3.0",
69
+ "@google-cloud/vertexai": "^1.4.0",
70
+ "@notionhq/client": "^2.2.15",
71
+ "exifr": "^7.1.3",
72
+ "ffmpeg-static": "^5.2.0",
73
+ "ffprobe-static": "^3.1.0",
74
+ "firebase-admin": "^11.10.1",
75
+ "fluent-ffmpeg": "^2.1.3",
76
+ "googleapis": "^144.0.0",
77
+ "multer": "^1.4.5-lts.1",
78
+ "oauth-signature": "1.5.0",
79
+ "socket.io": "^4.7.2"
80
+ }
81
+ }