@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 +7 -0
- package/app.js +1 -1
- package/channels/chat21/chat21WebHook.js +1 -1
- package/migrations/1771844588961-phone-channels-migration.js +30 -13
- package/package.json +1 -1
- package/pubmodules/emailNotification/requestNotification.js +42 -1
- package/pubmodules/pubModulesManager.js +4 -0
- package/routes/project_user.js +44 -3
- package/services/schemaMigrationService.js +11 -3
- package/utils/TdCache.js +12 -1
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.
|
|
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
|
|
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
|
|
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:
|
|
44
|
-
|
|
51
|
+
'channel.name': { $in: VOICE_TWILIO_CHANNEL_NAMES },
|
|
52
|
+
createdBy: { $regex: /^(voice-twilio-|voice_twilio-)/ }
|
|
45
53
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
@@ -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.");
|
package/routes/project_user.js
CHANGED
|
@@ -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 }
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|