@tiledesk/tiledesk-server 2.18.5 → 2.18.16

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.
@@ -2,6 +2,7 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { Namespace, AnsweredQuestion } = require('../models/kb_setting');
4
4
  const winston = require('../config/winston');
5
+ var fastCsv = require('fast-csv');
5
6
 
6
7
  // Add a new unanswerd question
7
8
  router.post('/', async (req, res) => {
@@ -210,6 +211,78 @@ router.get('/count/:namespace', async (req, res) => {
210
211
  }
211
212
  })
212
213
 
214
+ router.get('/:namespace/export', async (req, res) => {
215
+ try {
216
+ const { namespace } = req.params;
217
+ const id_project = req.projectid;
218
+ const mode = String(req.query.mode || 'csv').toLowerCase();
219
+
220
+ if (mode !== 'csv' && mode !== 'json') {
221
+ return res.status(400).json({
222
+ success: false,
223
+ error: 'Invalid format. Use mode=json or mode=csv'
224
+ });
225
+ }
226
+
227
+ const isValidNamespace = await validateNamespace(id_project, namespace);
228
+ if (!isValidNamespace) {
229
+ return res.status(403).json({
230
+ success: false,
231
+ error: "Not allowed. The namespace does not belong to the current project."
232
+ });
233
+ }
234
+
235
+ const questions = await AnsweredQuestion.find({ id_project, namespace })
236
+ .sort({ createdAt: -1 })
237
+ .lean();
238
+
239
+ const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
240
+
241
+ if (mode === 'json') {
242
+ const questionsJson = questions.map((q) => {
243
+ const { __v, updatedAt, ...doc } = q;
244
+ return doc;
245
+ });
246
+ const payload = {
247
+ namespace,
248
+ exportedAt: new Date().toISOString(),
249
+ count: questionsJson.length,
250
+ questions: questionsJson
251
+ };
252
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
253
+ res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.json"`);
254
+ return res.send(JSON.stringify(payload, null, 2));
255
+ }
256
+
257
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
258
+ res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.csv"`);
259
+
260
+ const csvStream = fastCsv.format({ headers: true });
261
+ csvStream.pipe(res);
262
+
263
+ for (const q of questions) {
264
+ csvStream.write({
265
+ id: String(q._id),
266
+ namespace: q.namespace,
267
+ question: q.question,
268
+ answer: q.answer,
269
+ tokens: q.tokens != null ? q.tokens : '',
270
+ request_id: q.request_id || '',
271
+ createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
272
+ });
273
+ }
274
+ csvStream.end();
275
+ } catch (error) {
276
+ winston.error('Error exporting answered questions:', error);
277
+ if (!res.headersSent) {
278
+ res.status(500).json({
279
+ success: false,
280
+ error: "Error exporting answered questions"
281
+ });
282
+ }
283
+ }
284
+ });
285
+
213
286
  // Helper function to validate namespace
214
287
  async function validateNamespace(id_project, namespace_id) {
215
288
  try {
package/routes/kb.js CHANGED
@@ -21,6 +21,7 @@ const Sitemapper = require('sitemapper');
21
21
  const aiService = require('../services/aiService');
22
22
  const aiManager = require('../services/aiManager');
23
23
  const integrationService = require('../services/integrationService');
24
+ const kbEvent = require('../event/kbEvent');
24
25
 
25
26
  const AMQP_MANAGER_URL = process.env.AMQP_MANAGER_URL;
26
27
  const JOB_TOPIC_EXCHANGE = process.env.JOB_TOPIC_EXCHANGE_TRAIN || 'tiledesk-trainer';
@@ -41,7 +42,6 @@ if (MAX_UPLOAD_FILE_SIZE) {
41
42
  }
42
43
  var upload = multer({limits: uploadlimits});
43
44
 
44
-
45
45
  let jobManager = new JobManager(AMQP_MANAGER_URL, {
46
46
  debug: false,
47
47
  topic: JOB_TOPIC_EXCHANGE,
@@ -835,6 +835,7 @@ router.delete('/deleteall', async (req, res) => {
835
835
 
836
836
  let project_id = req.projectid;
837
837
  let data = req.body;
838
+ let namespace_id = data.namespace;
838
839
  winston.debug('/delete all data: ', data);
839
840
 
840
841
  let namespace;
@@ -851,6 +852,7 @@ router.delete('/deleteall', async (req, res) => {
851
852
 
852
853
  aiService.deleteNamespace(data).then((resp) => {
853
854
  winston.debug("delete namespace resp: ", resp.data);
855
+ kbEvent.emit('kb.contents.delete', { req, namespace_id, project_id });
854
856
  res.status(200).send(resp.data);
855
857
  }).catch((err) => {
856
858
  winston.error("delete namespace err: ", err);
@@ -1133,6 +1135,8 @@ router.post('/namespace', async (req, res) => {
1133
1135
  return res.status(500).send({ success: false, error: err });
1134
1136
  }
1135
1137
 
1138
+ kbEvent.emit('kb.namespace.create', { req, savedNamespace, body, namespace_id, project_id });
1139
+
1136
1140
  let namespaceObj = savedNamespace.toObject();
1137
1141
  delete namespaceObj._id;
1138
1142
  delete namespaceObj.__v;
@@ -1349,6 +1353,8 @@ router.put('/namespace/:id', async (req, res) => {
1349
1353
  return res.status(500).send({ success: false, error: err });
1350
1354
  }
1351
1355
 
1356
+ kbEvent.emit('kb.namespace.update', { updatedNamespace, namespace_id });
1357
+
1352
1358
  let namespaceObj = updatedNamespace.toObject();
1353
1359
  delete namespaceObj._id;
1354
1360
  delete namespaceObj.__v;
@@ -1388,6 +1394,8 @@ router.delete('/namespace/:id', async (req, res) => {
1388
1394
  })
1389
1395
  winston.debug("delete all contents response: ", deleteResponse);
1390
1396
 
1397
+ kbEvent.emit('kb.contents.delete', { req, namespace_id, project_id, deletedCount: deleteResponse?.deletedCount });
1398
+
1391
1399
  return res.status(200).send({ success: true, message: "All contents deleted successfully" })
1392
1400
 
1393
1401
  }).catch((err) => {
@@ -2195,6 +2203,17 @@ router.delete('/:kb_id', async (req, res) => {
2195
2203
  return res.status(errorCode).send({ success: false, error: err.error });
2196
2204
  }
2197
2205
 
2206
+ if (kb.type === 'sitemap') {
2207
+ try {
2208
+ await aiManager.deleteSitemap(kb, namespace);
2209
+ console.log("Scheduled jobs for deleting sitemap");
2210
+ //return res.status(200).send({ success: true, message: "Scheduled jobs for deleting sitemap" });
2211
+ } catch (err) {
2212
+ winston.error("Error deleting sitemap: ", err);
2213
+ return res.status(500).send({ success: false, error: err });
2214
+ }
2215
+ }
2216
+
2198
2217
  let data = {
2199
2218
  id: kb_id,
2200
2219
  namespace: namespace_id
@@ -17,6 +17,7 @@ require('../middleware/passport')(passport);
17
17
  var validtoken = require('../middleware/valid-token')
18
18
  var roleChecker = require('../middleware/has-role');
19
19
  const puEvent = require('../event/projectUserEvent');
20
+ const { track } = require('../lib/analyticsClient');
20
21
 
21
22
 
22
23
  router.post('/invite', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('admin')], function (req, res) {
@@ -110,7 +111,7 @@ router.post('/invite', [passport.authenticate(['basic', 'jwt'], { session: false
110
111
 
111
112
  emailService.sendYouHaveBeenInvited(email, req.user.firstname, req.user.lastname, req.project.name, id_project, user.firstname, user.lastname, req.body.role)
112
113
 
113
- updatedPuser.populate({path:'id_user', select:{'firstname':1, 'lastname':1}},function (err, updatedPuserPopulated){
114
+ updatedPuser.populate({path:'id_user', select:{'firstname':1, 'lastname':1, 'email': 1}},function (err, updatedPuserPopulated){
114
115
  var pu = updatedPuserPopulated.toJSON();
115
116
  pu.isBusy = ProjectUserUtil.isBusy(savedProject_userPopulated, req.project.settings && req.project.settings.max_agent_assigned_chat);
116
117
  var eventData = {req:req, updatedPuserPopulated: pu};
@@ -141,7 +142,7 @@ router.post('/invite', [passport.authenticate(['basic', 'jwt'], { session: false
141
142
 
142
143
  emailService.sendYouHaveBeenInvited(email, req.user.firstname, req.user.lastname, req.project.name, id_project, user.firstname, user.lastname, req.body.role)
143
144
 
144
- savedProject_user.populate({path:'id_user', select:{'firstname':1, 'lastname':1}},function (err, savedProject_userPopulated){
145
+ savedProject_user.populate({path:'id_user', select:{'firstname':1, 'lastname':1, 'email': 1}},function (err, savedProject_userPopulated){
145
146
  var pu = savedProject_userPopulated.toJSON();
146
147
  pu.isBusy = ProjectUserUtil.isBusy(savedProject_userPopulated, req.project.settings && req.project.settings.max_agent_assigned_chat);
147
148
  var eventData = {req:req, savedProject_userPopulated: pu};
@@ -218,6 +219,8 @@ router.put('/', [passport.authenticate(['basic', 'jwt'], { session: false }), va
218
219
  update.tags = req.body.tags;
219
220
  }
220
221
 
222
+ const previousUserAvailable = req.projectuser.user_available;
223
+
221
224
  Project_user.findByIdAndUpdate(req.projectuser.id, update, { new: true, upsert: true }, function (err, updatedProject_user) {
222
225
  if (err) {
223
226
  winston.error("Error gettting project_user for update", err);
@@ -227,7 +230,7 @@ router.put('/', [passport.authenticate(['basic', 'jwt'], { session: false }), va
227
230
  updatedProject_user.populate({ path:'id_user', select: { 'firstname': 1, 'lastname': 1 }}, function (err, updatedProject_userPopulated) {
228
231
  var pu = updatedProject_userPopulated.toJSON();
229
232
  pu.isBusy = ProjectUserUtil.isBusy(updatedProject_userPopulated, req.project.settings && req.project.settings.max_agent_assigned_chat);
230
- authEvent.emit('project_user.update', {updatedProject_userPopulated:pu, req: req});
233
+ authEvent.emit('project_user.update', {updatedProject_userPopulated:pu, req: req, previousUserAvailable: previousUserAvailable});
231
234
  });
232
235
 
233
236
  res.json(updatedProject_user);
@@ -283,24 +286,34 @@ router.put('/:project_userid', [passport.authenticate(['basic', 'jwt'], { sessio
283
286
 
284
287
  winston.debug("project_userid update", update);
285
288
 
286
- Project_user.findByIdAndUpdate(req.params.project_userid, update, { new: true, upsert: true }, function (err, updatedProject_user) {
287
- if (err) {
288
- winston.error("Error gettting project_user for update", err);
289
- return res.status(500).send({ success: false, msg: 'Error updating object.' });
290
- }
291
- updatedProject_user.populate({path:'id_user', select:{'firstname':1, 'lastname':1}},function (err, updatedProject_userPopulated){
292
- if (err) {
293
- winston.error("Error gettting updatedProject_userPopulated for update", err);
294
- }
295
- var pu = updatedProject_userPopulated.toJSON();
296
- pu.isBusy = ProjectUserUtil.isBusy(updatedProject_user, req.project.settings && req.project.settings.max_agent_assigned_chat);
297
-
298
- authEvent.emit('project_user.update', {updatedProject_userPopulated:pu, req: req});
299
- });
300
-
289
+ function _doUpdateProjectUser(previousUserAvailable) {
290
+ Project_user.findByIdAndUpdate(req.params.project_userid, update, { new: true, upsert: true }, function (err, updatedProject_user) {
291
+ if (err) {
292
+ winston.error("Error gettting project_user for update", err);
293
+ return res.status(500).send({ success: false, msg: 'Error updating object.' });
294
+ }
295
+ updatedProject_user.populate({path:'id_user', select:{'firstname':1, 'lastname':1}},function (err, updatedProject_userPopulated){
296
+ if (err) {
297
+ winston.error("Error gettting updatedProject_userPopulated for update", err);
298
+ }
299
+ var pu = updatedProject_userPopulated.toJSON();
300
+ pu.isBusy = ProjectUserUtil.isBusy(updatedProject_user, req.project.settings && req.project.settings.max_agent_assigned_chat);
301
+
302
+ authEvent.emit('project_user.update', {updatedProject_userPopulated:pu, req: req, previousUserAvailable: previousUserAvailable});
303
+ });
304
+
301
305
 
302
- res.json(updatedProject_user);
303
- });
306
+ res.json(updatedProject_user);
307
+ });
308
+ }
309
+
310
+ if (update.user_available !== undefined && process.env.ANALYTICS_INGEST_URL) {
311
+ Project_user.findById(req.params.project_userid, 'user_available').lean().exec(function(err, currentPu) {
312
+ _doUpdateProjectUser(currentPu ? currentPu.user_available : null);
313
+ });
314
+ } else {
315
+ _doUpdateProjectUser(null);
316
+ }
304
317
  });
305
318
 
306
319
  // TODO fai servizio di patch degli attributi come request
@@ -372,6 +385,24 @@ router.delete('/:project_userid', [passport.authenticate(['basic', 'jwt'], { ses
372
385
 
373
386
  winston.debug("Disabled project_user", project_user);
374
387
 
388
+ // Populate id_user to obtain user_email for the analytics event.
389
+ // user_email is required as a valid email string by the contract
390
+ // (project-user-deactivated.ts) — skip the event when unavailable.
391
+ project_user.populate({ path: 'id_user', select: { 'email': 1 } }, function(populateErr, populatedPu) {
392
+ var puToUse = populatedPu || project_user;
393
+ var userObj = puToUse.id_user;
394
+ var email = (userObj && userObj.email) || null;
395
+
396
+ if (email) {
397
+ track('project_user.deactivated', puToUse.id_project, {
398
+ id_user: puToUse.uuid_user || puToUse._id.toString(),
399
+ user_email: email,
400
+ deactivated_by: (req.user && req.user.id) || null,
401
+ reason: null,
402
+ });
403
+ }
404
+ });
405
+
375
406
  // Event 'project_user.delete' not working - Check it and improve it to manage disable project user
376
407
  return res.status(200).send(project_user);
377
408
  });