@tiledesk/tiledesk-server 2.15.3 → 2.15.5

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
+ # 2.15.5
9
+ - Fixed email flooding when smart assignment is active and there are no operators available
10
+
11
+ # 2.15.4
12
+ - Updated endpoint to get project users adding the query parameter "trashed" in order to obtain also the trashed users.
13
+ - Added endpoint /restore to restore a deleted project user
14
+
8
15
  # 2.15.3
9
16
  - Updated whatsapp-connector to 1.0.25
10
17
  - Updated sms-connector to 0.1.13
package/app.js CHANGED
@@ -200,7 +200,7 @@ var faqBotHandler = require('./services/faqBotHandler');
200
200
  faqBotHandler.listen();
201
201
 
202
202
  var pubModulesManager = require('./pubmodules/pubModulesManager');
203
- pubModulesManager.init({express:express, mongoose:mongoose, passport:passport, databaseUri:databaseUri, routes:{}, jobsManager:jobsManager});
203
+ pubModulesManager.init({express:express, mongoose:mongoose, passport:passport, databaseUri:databaseUri, routes:{}, jobsManager:jobsManager, tdCache:tdCache});
204
204
 
205
205
  jobsManager.listen(); //listen after pubmodules to enabled queued *.queueEnabled events
206
206
 
@@ -798,7 +798,7 @@ else if (req.body.event_type == "presence-change") {
798
798
 
799
799
  // urgente Cannot read property '_id' of null at /usr/src/app/channels/chat21/chat21WebHook.js:663:68 a
800
800
  if (!updatedProject_userPopulated.id_project) {
801
- winston.warn('Error updatedProject_userPopulated.id_project not found.',{updatedProject_userPopulated:updatedProject_userPopulated, savedProjectUser:savedProjectUser,project_user:project_user});
801
+ winston.debug('Error updatedProject_userPopulated.id_project not found.',{updatedProject_userPopulated:updatedProject_userPopulated, savedProjectUser:savedProjectUser,project_user:project_user});
802
802
  // return res.status(404).send({ success: false, msg: 'Error updatedProject_userPopulated.id_project not found.' });
803
803
  // continue;
804
804
  } else {
@@ -2,9 +2,17 @@ var winston = require('../config/winston');
2
2
  const Request = require('../models/request');
3
3
  const phoneUtil = require('../utils/phoneUtil');
4
4
 
5
- const VOICE_CHANNEL_NAMES = ['voice_twilio', 'voice-twilio', 'voice-vxml', 'voice-vxml-enghouse'];
5
+ const VOICE_TWILIO_CHANNEL_NAMES = ['voice_twilio', 'voice-twilio'];
6
+ const VOICE_VXML_CHANNEL_NAMES = ['voice-vxml', 'voice-vxml-enghouse'];
6
7
  const BATCH_SIZE = 100;
7
8
 
9
+ function getPhoneFromVoiceTwilioCreatedBy(createdBy) {
10
+ if (!createdBy || typeof createdBy !== 'string') return null;
11
+ if (createdBy.startsWith('voice-twilio-')) return createdBy.replace(/^voice-twilio-/, '');
12
+ if (createdBy.startsWith('voice_twilio-')) return createdBy.replace(/^voice_twilio-/, '');
13
+ return null;
14
+ }
15
+
8
16
  async function updateManyWithNormalizedPhone(filter, getPhoneFromDoc) {
9
17
  let matched = 0;
10
18
  let modified = 0;
@@ -38,39 +46,48 @@ async function updateManyWithNormalizedPhone(filter, getPhoneFromDoc) {
38
46
 
39
47
  async function up() {
40
48
  try {
41
- // Voice channels
49
+ // Voice Twilio (voice-twilio / voice_twilio: phone from createdBy as "voice-twilio-{phone}" or "voice_twilio-{phone}")
42
50
  const voiceFilter = {
43
- 'channel.name': { $in: VOICE_CHANNEL_NAMES },
44
- 'attributes.caller_phone': { $exists: true, $nin: [null, ''] }
51
+ 'channel.name': { $in: VOICE_TWILIO_CHANNEL_NAMES },
52
+ createdBy: { $regex: /^(voice-twilio-|voice_twilio-)/ }
45
53
  };
46
- const voiceResult = await updateManyWithNormalizedPhone(voiceFilter, (doc) => doc.attributes?.caller_phone);
47
- winston.info(
48
- `[phone-channels-migration] Voice channels: matched ${voiceResult.matched}, modified ${voiceResult.modified}`
54
+ winston.info(`[phone-channels-migration] Voice Twilio Starting...`);
55
+ const voiceResult = await updateManyWithNormalizedPhone(voiceFilter, (doc) =>
56
+ getPhoneFromVoiceTwilioCreatedBy(doc.createdBy)
49
57
  );
58
+ winston.info(`[phone-channels-migration] Voice Twilio: matched ${voiceResult.matched}, modified ${voiceResult.modified}`);
59
+
60
+ // Voice VXML (voice-vxml / voice-vxml-enghouse: phone from attributes.caller_phone)
61
+ const voiceVxmlFilter = {
62
+ 'channel.name': { $in: VOICE_VXML_CHANNEL_NAMES },
63
+ 'attributes.caller_phone': { $exists: true, $nin: [null, ''] }
64
+ };
65
+ winston.info(`[phone-channels-migration] Voice VXML Starting...`);
66
+ const voiceVxmlResult = await updateManyWithNormalizedPhone(voiceVxmlFilter, (doc) => doc.attributes?.caller_phone);
67
+ winston.info(`[phone-channels-migration] Voice VXML: matched ${voiceVxmlResult.matched}, modified ${voiceVxmlResult.modified}`);
50
68
 
51
69
  // WhatsApp
52
70
  const wabFilter = {
53
71
  'channel.name': 'whatsapp',
54
72
  createdBy: { $regex: /^wab-/ }
55
73
  };
74
+ winston.info(`[phone-channels-migration] WhatsApp Starting...`);
56
75
  const wabResult = await updateManyWithNormalizedPhone(wabFilter, (doc) =>
57
76
  doc.createdBy && doc.createdBy.startsWith('wab-') ? doc.createdBy.replace(/^wab-/, '') : null
58
77
  );
59
- winston.info(
60
- `[phone-channels-migration] WhatsApp: matched ${wabResult.matched}, modified ${wabResult.modified}`
61
- );
78
+ winston.info(`[phone-channels-migration] WhatsApp: matched ${wabResult.matched}, modified ${wabResult.modified}`);
62
79
 
63
80
  // SMS-Twilio
64
81
  const smsFilter = {
65
82
  'channel.name': 'sms-twilio',
66
83
  createdBy: { $regex: /^sms-twilio-/ }
67
84
  };
85
+ winston.info(`[phone-channels-migration] SMS-Twilio Starting...`);
68
86
  const smsResult = await updateManyWithNormalizedPhone(smsFilter, (doc) =>
69
87
  doc.createdBy && doc.createdBy.startsWith('sms-twilio-') ? doc.createdBy.replace(/^sms-twilio-/, '') : null
70
88
  );
71
- winston.info(
72
- `[phone-channels-migration] SMS-Twilio: matched ${smsResult.matched}, modified ${smsResult.modified}`
73
- );
89
+ winston.info(`[phone-channels-migration] SMS-Twilio: matched ${smsResult.matched}, modified ${smsResult.modified}`);
90
+
74
91
  } catch (err) {
75
92
  winston.error('[phone-channels-migration] Error:', err);
76
93
  throw err;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.15.3",
4
+ "version": "2.15.5",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -14,6 +14,7 @@ var winston = require('../../config/winston');
14
14
  var RoleConstants = require("../../models/roleConstants");
15
15
  var ChannelConstants = require("../../models/channelConstants");
16
16
  var cacheUtil = require('../../utils/cacheUtil');
17
+ const { TdCache } = require('../../utils/TdCache');
17
18
 
18
19
  const messageEvent = require('../../event/messageEvent');
19
20
  var mongoose = require('mongoose');
@@ -40,6 +41,23 @@ if (pKey) {
40
41
  let apiUrl = process.env.API_URL || configGlobal.apiUrl;
41
42
  winston.debug('********* RequestNotification apiUrl: ' + apiUrl);
42
43
 
44
+ /** Redis cache for throttling pooled-request email. Injected by app (shared TdCache) or lazy-created fallback. */
45
+ let _pooledEmailTdCache = null;
46
+ function getPooledEmailCache() {
47
+ if (_pooledEmailTdCache) return _pooledEmailTdCache;
48
+ const fallback = new TdCache({
49
+ host: process.env.CACHE_REDIS_HOST,
50
+ port: process.env.CACHE_REDIS_PORT,
51
+ password: process.env.CACHE_REDIS_PASSWORD
52
+ });
53
+ fallback.connect();
54
+ _pooledEmailTdCache = fallback;
55
+ return _pooledEmailTdCache;
56
+ }
57
+
58
+ /** TTL in seconds: do not send pooled email again for the same request within this window (avoids flooding when smart assignment retries) */
59
+ const POOLED_EMAIL_THROTTLE_TTL = Number(process.env.POOLED_EMAIL_THROTTLE_TTL) || cacheUtil.defaultTTL;
60
+
43
61
  class RequestNotification {
44
62
 
45
63
  constructor() {
@@ -48,7 +66,18 @@ class RequestNotification {
48
66
  this.enabled = false;
49
67
  }
50
68
  winston.debug("RequestNotification this.enabled: "+ this.enabled);
51
- }
69
+ }
70
+
71
+ /**
72
+ * Reuse app's TdCache instance instead of opening a new Redis connection (optional, called by PubModulesManager).
73
+ * @param {TdCache} tdCache - shared Redis cache from app.get('redis_client')
74
+ */
75
+ setTdCache(tdCache) {
76
+ if (tdCache && typeof tdCache.setNX === 'function') {
77
+ _pooledEmailTdCache = tdCache;
78
+ winston.debug("RequestNotification using shared TdCache for pooled email throttle");
79
+ }
80
+ }
52
81
 
53
82
  listen() {
54
83
 
@@ -892,6 +921,18 @@ sendAgentEmail(projectid, savedRequest) {
892
921
  return winston.warn("RequestNotification savedRequest.snapshot is null :(. You are closing an old request?");
893
922
  }
894
923
 
924
+ // Throttle: send pooled email only once per request within TTL (avoids flooding when smart assignment keeps retrying)
925
+ const requestId = (savedRequest._id || savedRequest.id).toString();
926
+ const pooledEmailKey = 'pooled_request_email:' + requestId;
927
+ try {
928
+ const shouldSend = await getPooledEmailCache().setNX(pooledEmailKey, '1', POOLED_EMAIL_THROTTLE_TTL);
929
+ if (!shouldSend) {
930
+ return winston.debug("RequestNotification pooled email already sent for request " + requestId + " (Redis throttle, TTL " + POOLED_EMAIL_THROTTLE_TTL + "s)");
931
+ }
932
+ } catch (redisErr) {
933
+ winston.warn("RequestNotification Redis setNX for pooled email throttle failed, skipping send", { error: redisErr, requestId });
934
+ return;
935
+ }
895
936
 
896
937
 
897
938
  var snapshotAgents = savedRequest; //riassegno varibile cosi nn cambio righe successive
@@ -175,6 +175,7 @@ class PubModulesManager {
175
175
  winston.debug("PubModulesManager init");
176
176
 
177
177
  this.jobsManager = config.jobsManager;
178
+ this.tdCache = config.tdCache;
178
179
 
179
180
  try {
180
181
  this.appRules = require('./rules/appRules');
@@ -631,6 +632,9 @@ class PubModulesManager {
631
632
  // job_here
632
633
  if (this.emailNotification) {
633
634
  try {
635
+ if (this.tdCache) {
636
+ this.emailNotification.requestNotification.setTdCache(this.tdCache);
637
+ }
634
638
  // this.emailNotification.requestNotification.listen();
635
639
  this.jobsManager.listenEmailNotification(this.emailNotification);
636
640
  winston.info("PubModulesManager emailNotification started.");
@@ -344,11 +344,49 @@ router.delete('/:project_userid', [passport.authenticate(['basic', 'jwt'], { ses
344
344
 
345
345
  // Event 'project_user.delete' not working - Check it and improve it to manage disable project user
346
346
  return res.status(200).send(project_user);
347
- }
348
- );
347
+ });
349
348
  }
350
349
  });
351
350
 
351
+ // Restore a soft-deleted (trashed) project user. Fails if not found or not trashed.
352
+ router.put('/:project_userid/restore', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('admin')], function (req, res) {
353
+ const pu_id = req.params.project_userid;
354
+
355
+ Project_user.findOne({ _id: pu_id, id_project: req.projectid }, function (err, project_user) {
356
+ if (err) {
357
+ winston.error("Error finding project_user for restore", err);
358
+ return res.status(500).send({ success: false, msg: 'Error restoring Project User with id ' + pu_id });
359
+ }
360
+ if (!project_user) {
361
+ winston.warn("Project user not found for restore with id " + pu_id);
362
+ return res.status(404).send({ success: false, error: 'Project user not found with id ' + pu_id });
363
+ }
364
+ if (project_user.trashed !== true) {
365
+ winston.warn("Project user is not trashed, cannot restore id " + pu_id);
366
+ return res.status(400).send({ success: false, error: 'Project user is not trashed, cannot restore' });
367
+ }
368
+
369
+ Project_user.findByIdAndUpdate(pu_id, { trashed: false, status: 'active' }, { new: true }, function (err, updatedProject_user) {
370
+ if (err) {
371
+ winston.error("Error restoring project_user", err);
372
+ return res.status(500).send({ success: false, msg: 'Error restoring Project User with id ' + pu_id });
373
+ }
374
+
375
+ winston.debug("Restored project_user", updatedProject_user);
376
+ updatedProject_user.populate({ path: 'id_user', select: { 'firstname': 1, 'lastname': 1 } }, function (err, updatedProject_userPopulated) {
377
+ if (err) {
378
+ winston.error("Error populating restored project_user", err);
379
+ } else {
380
+ var pu = updatedProject_userPopulated.toJSON();
381
+ pu.isBusy = ProjectUserUtil.isBusy(updatedProject_user, req.project && req.project.settings && req.project.settings.max_agent_assigned_chat);
382
+ authEvent.emit('project_user.update', { updatedProject_userPopulated: pu, req: req });
383
+ }
384
+ });
385
+ return res.status(200).send(updatedProject_user);
386
+ });
387
+ });
388
+ });
389
+
352
390
  router.get('/:project_userid', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['subscription'])], function (req, res) {
353
391
  // router.get('/details/:project_userid', function (req, res) {
354
392
  // winston.debug("PROJECT USER ROUTES - req projectid", req.projectid);
@@ -482,7 +520,10 @@ router.get('/', [passport.authenticate(['basic', 'jwt'], { session: false }), va
482
520
  }
483
521
  winston.debug("role", role);
484
522
 
485
- var query = { id_project: req.projectid, role: { $in : role }, trashed: { $ne: true } };
523
+ var query = { id_project: req.projectid, role: { $in : role } };
524
+ if (!req.query.trashed || req.query.trashed === 'false' || req.query.trashed === false) {
525
+ query.trashed = { $ne: true };
526
+ }
486
527
 
487
528
  if (req.query.presencestatus) {
488
529
  query["presence.status"] = req.query.presencestatus;
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var path = require('path');
4
+ var fs = require('fs');
3
5
  var winston = require('../config/winston');
4
6
  var config = require('../config/database');
5
7
  var migrateMongoose = require('migrate-mongoose');
@@ -29,12 +31,13 @@ class SchamaMigrationService {
29
31
  var
30
32
  // migrationsDir = '/path/to/migrations/',
31
33
  // templatePath,
34
+ migrationsPath = path.resolve(process.cwd(), 'migrations'),
32
35
  dbUrl = process.env.DATABASE_URI || process.env.MONGODB_URI || config.database,
33
36
  collectionName = 'schemaMigrations',
34
37
  autosync = true;
35
38
 
36
39
  let migrator = new migrateMongoose({
37
- // migrationsPath: migrationsDir, // Path to migrations directory
40
+ migrationsPath: migrationsPath,
38
41
  // templatePath: templatePath, // The template to use when creating migrations needs up and down functions exposed
39
42
  dbConnectionUri: dbUrl, // mongo url
40
43
  collectionName: collectionName, // collection name to use for migrations (defaults to 'migrations')
@@ -44,13 +47,18 @@ class SchamaMigrationService {
44
47
  var list = await migrator.list();
45
48
  winston.debug("SchemaMigration script list", list);
46
49
 
47
- list.forEach(async(script)=> {
50
+ for (const script of list) {
48
51
  winston.debug("script", script);
52
+ const migrationFilePath = path.join(migrationsPath, script.filename);
53
+ if (!fs.existsSync(migrationFilePath)) {
54
+ winston.warn("SchemaMigration skipping migration (file not present): " + script.filename + " — remove the record from DB collection 'schemaMigrations' if not needed.");
55
+ continue;
56
+ }
49
57
  var runScript = await migrator.run('up', script.name);
50
58
  if (runScript && !this.isEmptyObject(runScript)) {
51
59
  winston.info("SchemaMigration script " + script.name + " executed.");
52
60
  }
53
- });
61
+ }
54
62
  // // Create a new migration
55
63
  // migrator.create(migrationName).then(()=> {
56
64
  // console.log(`Migration created. Run `+ `mongoose-migrate up ${migrationName}`.cyan + ` to apply the migration.`);
package/utils/TdCache.js CHANGED
@@ -76,7 +76,18 @@ class TdCache {
76
76
  });
77
77
  }
78
78
 
79
-
79
+ /**
80
+ * Set key only if it does not exist, with TTL (seconds). Returns true if key was set, false if key already existed.
81
+ * Uses Redis SET key value EX ttl NX to avoid email/notification flooding.
82
+ */
83
+ async setNX(key, value, ttlSeconds) {
84
+ return new Promise((resolve, reject) => {
85
+ this.client.set(key, value, 'EX', ttlSeconds, 'NX', (err, reply) => {
86
+ if (err) return reject(err);
87
+ resolve(reply === 'OK');
88
+ });
89
+ });
90
+ }
80
91
 
81
92
 
82
93