@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 +82 -82
- package/lib/cjs/chat/api/conversation.js +87 -5
- 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 +55 -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/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.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
|
-
|
|
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":"
|
|
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"}
|
package/lib/cjs/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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.
|
|
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/
|
|
41
|
-
"@types/
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"eslint
|
|
45
|
-
"eslint-
|
|
46
|
-
"eslint-plugin-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"@google-cloud/
|
|
68
|
-
"@
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
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
|
+
}
|