@runnerpro/backend 1.17.0 → 1.17.2
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 +82 -82
- package/lib/cjs/chat/api/conversation.js +42 -2
- package/lib/cjs/chat/api/listConversations.js +12 -12
- package/lib/cjs/chat/saveResponseTime.js +5 -5
- package/lib/cjs/chat/utils/getCountNotificaciones.js +12 -12
- package/lib/cjs/index.js +6 -1
- package/lib/cjs/mediaProcessing/index.js +255 -0
- package/lib/cjs/sendMail/index.js +65 -65
- package/lib/cjs/types/chat/api/conversation.d.ts +28 -1
- package/lib/cjs/types/chat/api/conversation.d.ts.map +1 -1
- package/lib/cjs/types/index.d.ts +2 -1
- package/lib/cjs/types/index.d.ts.map +1 -1
- package/lib/cjs/types/mediaProcessing/index.d.ts +65 -0
- package/lib/cjs/types/mediaProcessing/index.d.ts.map +1 -0
- package/lib/cjs/types/workout/planificacionPrueba7dias/10k/getPlanificacionPrueba7dias10k.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/10k/getPlanificacionPrueba7dias10kMas50.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/10k/getPlanificacionPrueba7dias10kPrimer.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/10k/getPlanificacionPrueba7dias10kSub50.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/21k/getPlanificacionPrueba7dias21k.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/21k/getPlanificacionPrueba7dias21kMas105.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/21k/getPlanificacionPrueba7dias21kPrimer.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/21k/getPlanificacionPrueba7dias21kSub105.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42k.d.ts +51 -2
- package/lib/cjs/types/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42k.d.ts.map +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42kMas210.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42kPrimer.d.ts +19 -2
- package/lib/cjs/types/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42kPrimer.d.ts.map +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42kSub210.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/5k/getPlanificacionPrueba7dias5k.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/5k/getPlanificacionPrueba7dias5kPrimer.d.ts +1 -1
- package/lib/cjs/types/workout/planificacionPrueba7dias/5k/getPlanificacionPrueba7dias5kSub30.d.ts +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/10k/getPlanificacionPrueba7dias10kMas50.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/10k/getPlanificacionPrueba7dias10kPrimer.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/10k/getPlanificacionPrueba7dias10kSub50.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/21k/getPlanificacionPrueba7dias21kMas105.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/21k/getPlanificacionPrueba7dias21kPrimer.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/21k/getPlanificacionPrueba7dias21kSub105.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42kMas210.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42kPrimer.js +1 -0
- package/lib/cjs/workout/planificacionPrueba7dias/42k/getPlanificacionPrueba7dias42kSub210.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/5k/getPlanificacionPrueba7dias5kPrimer.js +1 -1
- package/lib/cjs/workout/planificacionPrueba7dias/5k/getPlanificacionPrueba7dias5kSub30.js +1 -1
- package/package.json +81 -79
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.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) => {
|
|
@@ -239,8 +240,34 @@ const getChatFile = ({ bucket, id, isClient, userid }) => __awaiter(void 0, void
|
|
|
239
240
|
return null;
|
|
240
241
|
}
|
|
241
242
|
});
|
|
243
|
+
/**
|
|
244
|
+
* Envía un mensaje de chat entre entrenador y cliente
|
|
245
|
+
*
|
|
246
|
+
* @param req - Request con body conteniendo:
|
|
247
|
+
* @param req.body.text - Texto del mensaje
|
|
248
|
+
* @param req.body.idCliente - ID del cliente destinatario (solo para entrenadores)
|
|
249
|
+
* @param req.body.replyMessageId - ID del mensaje al que se responde (opcional)
|
|
250
|
+
* @param req.body.new - Objeto con datos de comentario intelligence (opcional, tipo 6)
|
|
251
|
+
* @param req.body.idWorkout - ID del workout a linkear en el mensaje (opcional, solo entrenadores)
|
|
252
|
+
* @param res - Response
|
|
253
|
+
* @param params.sendNotification - Función para enviar notificaciones push
|
|
254
|
+
* @param params.firebaseMessaging - Instancia de Firebase Messaging
|
|
255
|
+
* @param params.isClient - Si el sender es cliente (true) o entrenador (false)
|
|
256
|
+
* @returns Promise<void> - Envía { idMessage, text } en la respuesta
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* // Mensaje normal de entrenador
|
|
261
|
+
* POST /chat/conversation/send
|
|
262
|
+
* { "idCliente": "abc123", "text": "Hola!" }
|
|
263
|
+
*
|
|
264
|
+
* // Mensaje con workout linkeado
|
|
265
|
+
* POST /chat/conversation/send
|
|
266
|
+
* { "idCliente": "abc123", "text": "Para el entreno de series...", "idWorkout": "workout456" }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
242
269
|
const sendMessage = (req, res, { sendNotification, firebaseMessaging, isClient }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
243
|
-
const { replyMessageId, new: newMessage } = req.body;
|
|
270
|
+
const { replyMessageId, new: newMessage, idWorkout: idWorkoutBody } = req.body;
|
|
244
271
|
let { text } = req.body;
|
|
245
272
|
const { userid } = req.session;
|
|
246
273
|
const idCliente = isClient ? req.session.userid : req.body.idCliente;
|
|
@@ -264,6 +291,14 @@ const sendMessage = (req, res, { sendNotification, firebaseMessaging, isClient }
|
|
|
264
291
|
type = 6;
|
|
265
292
|
idWorkout = newMessage.idWorkout;
|
|
266
293
|
}
|
|
294
|
+
// ✅ Linkeo de workout desde el chat (cuando no es tipo intelligence)
|
|
295
|
+
// Solo para entrenadores (!isClient) - los clientes no pueden linkear workouts
|
|
296
|
+
if (!idWorkout && idWorkoutBody && !isClient) {
|
|
297
|
+
const [workout] = yield (0, index_1.query)('SELECT [ID] FROM [WORKOUT] WHERE [ID] = ? AND [ID CLIENTE] = ?', [idWorkoutBody, idCliente]);
|
|
298
|
+
if (workout) {
|
|
299
|
+
idWorkout = idWorkoutBody;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
267
302
|
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
303
|
res.send({ idMessage: message.id, text: textSpanish });
|
|
269
304
|
if (!isClient) {
|
|
@@ -288,6 +323,7 @@ const sendMessage = (req, res, { sendNotification, firebaseMessaging, isClient }
|
|
|
288
323
|
yield updateSenderView({ userid, idCliente, idMessage: message.id });
|
|
289
324
|
}
|
|
290
325
|
});
|
|
326
|
+
exports.sendMessage = sendMessage;
|
|
291
327
|
const updateSenderView = ({ userid, idCliente, idMessage }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
292
328
|
const [cliente] = yield (0, index_1.query)('SELECT [ID ENTRENADOR PRINCIPAL], [ID ENTRENADOR SECUNDARIO] FROM [CLIENTE] WHERE [ID] = ?', [idCliente]);
|
|
293
329
|
let idSenderView;
|
|
@@ -394,6 +430,10 @@ const sendFile = (req, res, { sendNotification, firebaseMessaging, isClient, buc
|
|
|
394
430
|
yield bucket.file(`Chat/${file.id}`).save(file.data);
|
|
395
431
|
}
|
|
396
432
|
fs_1.default.unlinkSync(filePath);
|
|
433
|
+
// ✅ Procesar archivo multimedia en background (transcripción de audio / descripción de imagen)
|
|
434
|
+
if (req.file.mimetype.startsWith('audio/') || req.file.mimetype.startsWith('image/')) {
|
|
435
|
+
(0, mediaProcessing_1.processMediaFile)(idFile, fileData, req.file.mimetype);
|
|
436
|
+
}
|
|
397
437
|
if (!isClient) {
|
|
398
438
|
let textFile = 'Archivo adjunto';
|
|
399
439
|
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;
|