@tiledesk/tiledesk-server 2.26.0 → 2.27.1
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 +6 -0
- package/channels/chat21/chat21Handler.js +553 -877
- package/jobs.js +91 -89
- package/jobsManager.js +58 -86
- package/models/chat21SyncTrace.js +53 -0
- package/package.json +1 -1
- package/pubmodules/activities/activityArchiver.js +1 -2
- package/pubmodules/queue/chat21WebhookWorker.js +145 -0
- package/pubmodules/queue/queueTopicsFilter.js +36 -0
- package/pubmodules/queue/reconnect.js +84 -134
- package/pubmodules/queue/test/queueWorker.test.js +131 -0
- package/services/chat21SyncTracer.js +110 -0
- package/services/requestService.js +58 -1
package/jobs.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point of the jobs worker (Dockerfile-jobs: `node jobs.js`).
|
|
3
|
+
* It runs the jobs that the server (app.js) skips when JOB_WORKER_ENABLED=true.
|
|
4
|
+
*
|
|
5
|
+
* Worker-specific env variables:
|
|
6
|
+
* - INCLUDE_QUEUE / EXCLUDE_QUEUE: routing keys consumed from RabbitMQ (see pubmodules/queue/reconnect.js)
|
|
7
|
+
* - TASK_SCHEDULER_ENABLED=false: disables the cron tasks (see pubmodules/scheduler/taskRunner.js)
|
|
8
|
+
* - MULTI_WORKER_ENABLED=false: disables @tiledesk/tiledesk-multi-worker
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------- Env ----------
|
|
1
12
|
|
|
2
13
|
var dotenvPath = undefined;
|
|
3
14
|
|
|
@@ -6,43 +17,35 @@ if (process.env.DOTENV_PATH) {
|
|
|
6
17
|
console.log("load dotenv form DOTENV_PATH", dotenvPath);
|
|
7
18
|
}
|
|
8
19
|
|
|
9
|
-
if (process.env.LOAD_DOTENV_SUBFOLDER
|
|
20
|
+
if (process.env.LOAD_DOTENV_SUBFOLDER) {
|
|
10
21
|
console.log("load dotenv form LOAD_DOTENV_SUBFOLDER");
|
|
11
|
-
dotenvPath = __dirname+'/confenv/.env';
|
|
22
|
+
dotenvPath = __dirname + '/confenv/.env';
|
|
12
23
|
}
|
|
13
24
|
|
|
14
|
-
require('dotenv').config({ path: dotenvPath});
|
|
25
|
+
require('dotenv').config({ path: dotenvPath });
|
|
15
26
|
|
|
27
|
+
// This process is the worker: modules reading JOB_WORKER_ENABLED must run their jobs here.
|
|
28
|
+
process.env.JOB_WORKER_ENABLED = false;
|
|
16
29
|
|
|
17
30
|
var mongoose = require('mongoose');
|
|
18
|
-
|
|
19
31
|
let winston = require('./config/winston');
|
|
32
|
+
var config = require('./config/database');
|
|
33
|
+
const { TdCache } = require('./utils/TdCache');
|
|
20
34
|
let JobsManager = require('./jobsManager');
|
|
21
35
|
|
|
22
|
-
|
|
23
36
|
let geoService = require('./services/geoService');
|
|
24
|
-
// let subscriptionNotifier = require('./services/subscriptionNotifier');
|
|
25
37
|
var subscriptionNotifierQueued = require('./services/subscriptionNotifierQueued');
|
|
26
38
|
var botSubscriptionNotifier = require('./services/BotSubscriptionNotifier');
|
|
27
|
-
|
|
28
|
-
const botEvent = require('./event/botEvent');
|
|
29
|
-
var channelManager = require('./channels/channelManager');
|
|
30
|
-
|
|
31
39
|
var updateLeadQueued = require('./services/updateLeadQueued');
|
|
32
40
|
var updateRequestSnapshotQueued = require('./services/updateRequestSnapshotQueued');
|
|
33
|
-
|
|
41
|
+
const botEvent = require('./event/botEvent');
|
|
42
|
+
var channelManager = require('./channels/channelManager');
|
|
34
43
|
|
|
35
44
|
require('./services/mongoose-cache-fn')(mongoose);
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
var config = require('./config/database');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//override JOB_WORKER_ENABLED to false when you start jobs.js
|
|
42
|
-
process.env.JOB_WORKER_ENABLED=false
|
|
46
|
+
// ---------- MongoDB ----------
|
|
43
47
|
|
|
44
48
|
var databaseUri = process.env.DATABASE_URI || process.env.MONGODB_URI || config.database;
|
|
45
|
-
var autoIndex = true;
|
|
46
49
|
|
|
47
50
|
if (!databaseUri) { //TODO??
|
|
48
51
|
winston.warn('DATABASE_URI not specified, falling back to localhost.');
|
|
@@ -51,103 +54,102 @@ if (!databaseUri) { //TODO??
|
|
|
51
54
|
let useUnifiedTopology = process.env.MONGOOSE_UNIFIED_TOPOLOGY === 'true';
|
|
52
55
|
winston.info("DB useUnifiedTopology: ", useUnifiedTopology, typeof useUnifiedTopology);
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
if (err) {
|
|
57
|
+
mongoose.connect(databaseUri, { "useNewUrlParser": true, "autoIndex": true, "useUnifiedTopology": useUnifiedTopology }, function (err) {
|
|
58
|
+
if (err) {
|
|
56
59
|
winston.error('Failed to connect to MongoDB on ' + databaseUri + " ", err);
|
|
57
60
|
process.exit(1);
|
|
58
61
|
}
|
|
59
|
-
winston.info("Mongoose connection done on host: "+mongoose.connection.host + " on port: " + mongoose.connection.port + " with name: "+ mongoose.connection.name)
|
|
62
|
+
winston.info("Mongoose connection done on host: " + mongoose.connection.host + " on port: " + mongoose.connection.port + " with name: " + mongoose.connection.name);
|
|
60
63
|
});
|
|
61
|
-
// winston.info("mongoose.connection",mongoose.connection);
|
|
62
|
-
// module.exports = jobsManager;
|
|
63
64
|
|
|
65
|
+
// ---------- Modules ----------
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Modules started directly by the worker, regardless of JobsManager.
|
|
69
|
+
*/
|
|
70
|
+
function startCoreModules() {
|
|
71
|
+
// Mongoose query cache.
|
|
72
|
+
require('./pubmodules/cache').cachegoose(mongoose);
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
// Concierge bot.
|
|
75
|
+
require('./pubmodules/rules/appRules').start();
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
// RabbitMQ publisher and consumers: the jobs queue and the chat21 webhook queue,
|
|
78
|
+
// filtered by INCLUDE_QUEUE / EXCLUDE_QUEUE.
|
|
79
|
+
require('./pubmodules/queue');
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
// chat21Handler listens to the standard events (request.create, not request.create.queue)
|
|
82
|
+
// because the worker emits them when it closes a request.
|
|
83
|
+
channelManager.listen();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Job listeners started through JobsManager.
|
|
88
|
+
* Most of them only react to the *.queue events emitted by pubmodules/queue, so they do nothing
|
|
89
|
+
* when their routing keys are excluded. The scheduler and the multi-worker run on their own.
|
|
90
|
+
*/
|
|
91
|
+
function startJobListeners(tdCache) {
|
|
92
|
+
let jobsManager = new JobsManager(undefined, geoService, botEvent, subscriptionNotifierQueued, botSubscriptionNotifier, updateLeadQueued, updateRequestSnapshotQueued);
|
|
75
93
|
|
|
94
|
+
jobsManager.listen();
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
jobsManager.listenEmailNotification(require('./pubmodules/emailNotification'));
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
require('./pubmodules/queue');
|
|
82
|
-
// require('@tiledesk-ent/tiledesk-server-queue');
|
|
98
|
+
jobsManager.listenActivityArchiver(require('./pubmodules/activities').activityArchiver);
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
let routingQueueQueued = require('./pubmodules/routing-queue').listenerQueued;
|
|
101
|
+
routingQueueQueued.setTdCache(tdCache);
|
|
102
|
+
jobsManager.listenRoutingQueue(routingQueueQueued);
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
const { TdCache } = require('./utils/TdCache');
|
|
89
|
-
let tdCache = new TdCache({
|
|
90
|
-
host: process.env.CACHE_REDIS_HOST,
|
|
91
|
-
port: process.env.CACHE_REDIS_PORT,
|
|
92
|
-
password: process.env.CACHE_REDIS_PASSWORD
|
|
93
|
-
});
|
|
94
|
-
tdCache.connect();
|
|
104
|
+
jobsManager.listenWhatsappQueue(require('@tiledesk/tiledesk-whatsapp-jobworker'));
|
|
95
105
|
|
|
106
|
+
// Cron tasks, disabled by TASK_SCHEDULER_ENABLED=false.
|
|
107
|
+
jobsManager.listenScheduler(require('./pubmodules/scheduler'));
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
// Consumes its own RabbitMQ queues on AMQP_MANAGER_URL, disabled by MULTI_WORKER_ENABLED=false.
|
|
110
|
+
jobsManager.listenMultiWorker(require('@tiledesk/tiledesk-multi-worker'));
|
|
111
|
+
}
|
|
98
112
|
|
|
99
|
-
|
|
113
|
+
function startAnalyticsPublisher() {
|
|
114
|
+
try {
|
|
115
|
+
require('./pubmodules/analytics-publisher').listen();
|
|
116
|
+
winston.info("AnalyticsPublisher initialized.");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
119
|
+
winston.info("PubModulesManager init analyticsPublisher module not found");
|
|
120
|
+
} else {
|
|
121
|
+
winston.info("PubModulesManager error initializing analyticsPublisher module", err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
100
125
|
|
|
126
|
+
// ---------- Main ----------
|
|
101
127
|
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
async function main() {
|
|
129
|
+
startCoreModules();
|
|
104
130
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
// Same Redis client as app.js, required by routing-queue (flow attributes cache delete).
|
|
132
|
+
let tdCache = new TdCache({
|
|
133
|
+
host: process.env.CACHE_REDIS_HOST,
|
|
134
|
+
port: process.env.CACHE_REDIS_PORT,
|
|
135
|
+
password: process.env.CACHE_REDIS_PASSWORD
|
|
136
|
+
});
|
|
137
|
+
tdCache.connect();
|
|
109
138
|
|
|
139
|
+
startJobListeners(tdCache);
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
winston.debug("routingQueueQueued");
|
|
113
|
-
if (tdCache) {
|
|
114
|
-
routingQueueQueued.setTdCache(tdCache);
|
|
115
|
-
}
|
|
116
|
-
jobsManager.listenRoutingQueue(routingQueueQueued);
|
|
117
|
-
|
|
118
|
-
let whatsappQueue = require('@tiledesk/tiledesk-whatsapp-jobworker');
|
|
119
|
-
winston.info("whatsappQueue");
|
|
120
|
-
jobsManager.listenWhatsappQueue(whatsappQueue);
|
|
121
|
-
|
|
122
|
-
let scheduler = require('./pubmodules/scheduler');
|
|
123
|
-
jobsManager.listenScheduler(scheduler);
|
|
124
|
-
|
|
125
|
-
let multiWorkerQueue = require('@tiledesk/tiledesk-multi-worker');
|
|
126
|
-
jobsManager.listenMultiWorker(multiWorkerQueue);
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
this.analyticsPublisher = require('./pubmodules/analytics-publisher');
|
|
130
|
-
this.analyticsPublisher.listen();
|
|
131
|
-
winston.info("AnalyticsPublisher initialized.");
|
|
132
|
-
} catch(err) {
|
|
133
|
-
if (err.code === 'MODULE_NOT_FOUND') {
|
|
134
|
-
winston.info("PubModulesManager init analyticsPublisher module not found");
|
|
135
|
-
} else {
|
|
136
|
-
winston.info("PubModulesManager error initializing analyticsPublisher module", err);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
141
|
+
startAnalyticsPublisher();
|
|
139
142
|
|
|
140
|
-
|
|
143
|
+
winston.info("Jobs started");
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
// Keeps the process alive.
|
|
146
|
+
await new Promise(function () {});
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
function panic(error)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
process.exit(1);
|
|
149
|
+
function panic(error) {
|
|
150
|
+
console.error(error);
|
|
151
|
+
process.exit(1);
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
// https://stackoverflow.com/a/46916601/1478566
|
|
153
|
-
main().catch(panic).finally(clearInterval.bind(null, setInterval(a=>a, 1E9)));
|
|
155
|
+
main().catch(panic).finally(clearInterval.bind(null, setInterval(a => a, 1E9)));
|
package/jobsManager.js
CHANGED
|
@@ -1,118 +1,90 @@
|
|
|
1
|
-
|
|
2
1
|
var winston = require('./config/winston');
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Starts the queued job listeners, shared by the two processes:
|
|
5
|
+
* - app.js (server): jobWorkerEnabled = JOB_WORKER_ENABLED. When true every listener is skipped,
|
|
6
|
+
* because the jobs are run by the external worker.
|
|
7
|
+
* - jobs.js (worker): jobWorkerEnabled = undefined, so every listener is started.
|
|
8
|
+
*/
|
|
4
9
|
class JobsManager {
|
|
5
10
|
constructor(jobWorkerEnabled, geoService, botEvent, subscriptionNotifierQueued, botSubscriptionNotifier, updateLeadQueued, updateRequestSnapshotQueued) {
|
|
6
|
-
this.geoService = geoService;
|
|
7
|
-
this.botEvent = botEvent;
|
|
8
|
-
// this.subscriptionNotifier = subscriptionNotifier;
|
|
9
|
-
this.subscriptionNotifierQueued = subscriptionNotifierQueued;
|
|
10
|
-
this.botSubscriptionNotifier = botSubscriptionNotifier;
|
|
11
|
-
|
|
12
|
-
this.emailNotificatio = undefined;
|
|
13
|
-
this.activityArchiver = undefined;
|
|
14
|
-
this.whatsappWorker = undefined;
|
|
15
|
-
this.multiWorkerQueue = undefined;
|
|
16
|
-
|
|
17
11
|
this.jobWorkerEnabled = jobWorkerEnabled;
|
|
18
|
-
// this.jobWorkerEnabled = false;
|
|
19
|
-
// if (process.env.JOB_WORKER_ENABLED=="true" || process.env.JOB_WORKER_ENABLED == true) {
|
|
20
|
-
// this.jobWorkerEnabled = true;
|
|
21
|
-
// }
|
|
22
|
-
// winston.info("JobsManager jobWorkerEnabled: "+ this.jobWorkerEnabled);
|
|
23
12
|
|
|
13
|
+
this.geoService = geoService;
|
|
14
|
+
this.subscriptionNotifierQueued = subscriptionNotifierQueued;
|
|
24
15
|
this.updateLeadQueued = updateLeadQueued;
|
|
25
16
|
this.updateRequestSnapshotQueued = updateRequestSnapshotQueued;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
listen() {
|
|
30
|
-
winston.info("JobsManager listener started");
|
|
31
|
-
if ( this.jobWorkerEnabled == true) {
|
|
32
|
-
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listeners");
|
|
33
|
-
}
|
|
34
|
-
this.geoService.listen();
|
|
35
|
-
|
|
36
|
-
// this.botEvent.listen(); // disabled
|
|
37
|
-
|
|
38
|
-
// this.subscriptionNotifier.start();
|
|
39
|
-
this.subscriptionNotifierQueued.start();
|
|
40
17
|
|
|
41
|
-
|
|
18
|
+
// Kept for the constructor signature, their listeners are disabled.
|
|
19
|
+
this.botEvent = botEvent;
|
|
20
|
+
this.botSubscriptionNotifier = botSubscriptionNotifier;
|
|
21
|
+
}
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Runs startFn unless the jobs are delegated to the external worker.
|
|
25
|
+
*/
|
|
26
|
+
start(name, startFn) {
|
|
27
|
+
if (this.jobWorkerEnabled == true) {
|
|
28
|
+
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping " + name);
|
|
45
29
|
}
|
|
30
|
+
winston.info("JobsManager starting " + name);
|
|
31
|
+
startFn();
|
|
32
|
+
}
|
|
46
33
|
|
|
47
|
-
|
|
34
|
+
listen() {
|
|
35
|
+
this.start("geo, subscription notifier, lead and request snapshot listeners", () => {
|
|
36
|
+
this.geoService.listen();
|
|
37
|
+
this.subscriptionNotifierQueued.start();
|
|
38
|
+
this.updateLeadQueued.listen();
|
|
39
|
+
if (this.updateRequestSnapshotQueued) {
|
|
40
|
+
this.updateRequestSnapshotQueued.listen();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
listenEmailNotification(emailNotification) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
this.emailNotification = emailNotification;
|
|
56
|
-
this.emailNotification.requestNotification.listen();
|
|
45
|
+
listenEmailNotification(emailNotification) {
|
|
46
|
+
this.start("Email Notification", () => {
|
|
47
|
+
this.emailNotification = emailNotification;
|
|
48
|
+
this.emailNotification.requestNotification.listen();
|
|
49
|
+
});
|
|
57
50
|
}
|
|
58
51
|
|
|
59
52
|
listenRoutingQueue(routingQueue) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
this.routingQueue = routingQueue;
|
|
65
|
-
this.routingQueue.listen();
|
|
53
|
+
this.start("Routing Queue", () => {
|
|
54
|
+
this.routingQueue = routingQueue;
|
|
55
|
+
this.routingQueue.listen();
|
|
56
|
+
});
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
listenScheduler(scheduler) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
this.scheduler = scheduler;
|
|
74
|
-
this.scheduler.taskRunner.start();
|
|
60
|
+
this.start("Scheduler", () => {
|
|
61
|
+
this.scheduler = scheduler;
|
|
62
|
+
this.scheduler.taskRunner.start();
|
|
63
|
+
});
|
|
75
64
|
}
|
|
76
65
|
|
|
77
66
|
listenActivityArchiver(activityArchiver) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listener for Activity Archiver");
|
|
83
|
-
}
|
|
84
|
-
this.activityArchiver = activityArchiver;
|
|
85
|
-
this.activityArchiver.listen();
|
|
67
|
+
this.start("Activity Archiver", () => {
|
|
68
|
+
this.activityArchiver = activityArchiver;
|
|
69
|
+
this.activityArchiver.listen();
|
|
70
|
+
});
|
|
86
71
|
}
|
|
87
72
|
|
|
88
73
|
listenWhatsappQueue(whatsappQueue) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listener for Whatsapp Queue");
|
|
93
|
-
}
|
|
94
|
-
// this.whatsappWorker = whatsappQueue;
|
|
95
|
-
// this.whatsappQueue.listen(); // oppure codice
|
|
74
|
+
this.start("Whatsapp Queue", () => {
|
|
75
|
+
// No listener to start yet.
|
|
76
|
+
});
|
|
96
77
|
}
|
|
97
78
|
|
|
98
79
|
listenMultiWorker(multiWorkerQueue) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
80
|
+
this.start("MultiWorker Queue", () => {
|
|
81
|
+
if (process.env.MULTI_WORKER_ENABLED === "false") {
|
|
82
|
+
return winston.info("JobsManager MULTI_WORKER_ENABLED is false. Skipping MultiWorker Queue");
|
|
83
|
+
}
|
|
84
|
+
this.multiWorkerQueue = multiWorkerQueue;
|
|
85
|
+
this.multiWorkerQueue.startJobsWorker();
|
|
86
|
+
});
|
|
106
87
|
}
|
|
107
|
-
|
|
108
|
-
// listenTrainingQueue(trainingQueue) {
|
|
109
|
-
// console.log("JobsManager listenTrainingQueue started");
|
|
110
|
-
// console.log("trainingQueue is: ", trainingQueue)
|
|
111
|
-
// if (this.jobWorkerEnabled == true) {
|
|
112
|
-
// return winston.info("JobsManager jobWorkerEnabled is enabled. Skipping listener for Training Queue");
|
|
113
|
-
// }
|
|
114
|
-
// }
|
|
115
88
|
}
|
|
116
89
|
|
|
117
|
-
|
|
118
|
-
module.exports = JobsManager;
|
|
90
|
+
module.exports = JobsManager;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
const Schema = mongoose.Schema;
|
|
3
|
+
|
|
4
|
+
const TTL_SECONDS = 60 * 60 * 24 * 7; // 1 week
|
|
5
|
+
|
|
6
|
+
const Chat21SyncTraceSchema = new Schema(
|
|
7
|
+
{
|
|
8
|
+
request_id: {
|
|
9
|
+
type: String,
|
|
10
|
+
required: false
|
|
11
|
+
},
|
|
12
|
+
id_project: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: false
|
|
15
|
+
},
|
|
16
|
+
event: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true
|
|
19
|
+
},
|
|
20
|
+
step: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
outcome: {
|
|
25
|
+
type: String,
|
|
26
|
+
enum: ['info', 'success', 'error', 'skipped'],
|
|
27
|
+
default: 'info'
|
|
28
|
+
},
|
|
29
|
+
data: {
|
|
30
|
+
type: Schema.Types.Mixed,
|
|
31
|
+
required: false
|
|
32
|
+
},
|
|
33
|
+
host: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: false
|
|
36
|
+
},
|
|
37
|
+
pid: {
|
|
38
|
+
type: Number,
|
|
39
|
+
required: false
|
|
40
|
+
},
|
|
41
|
+
createdAt: {
|
|
42
|
+
type: Date,
|
|
43
|
+
default: Date.now
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
Chat21SyncTraceSchema.index({ request_id: 1, createdAt: 1 });
|
|
49
|
+
Chat21SyncTraceSchema.index({ createdAt: 1 }, { expireAfterSeconds: TTL_SECONDS });
|
|
50
|
+
|
|
51
|
+
const Chat21SyncTrace = mongoose.model('Chat21SyncTrace', Chat21SyncTraceSchema);
|
|
52
|
+
|
|
53
|
+
module.exports = Chat21SyncTrace;
|
package/package.json
CHANGED
|
@@ -149,11 +149,10 @@ class ActivityArchiver {
|
|
|
149
149
|
listen() {
|
|
150
150
|
|
|
151
151
|
winston.debug('ActivityArchiver listen');
|
|
152
|
-
console.log("\n\nActivityArchiver listen");
|
|
153
152
|
|
|
154
153
|
const enabled = process.env.ACTIVITY_HISTORY_ENABLED || "false";
|
|
155
154
|
winston.debug('ActivityArchiver enabled:' + enabled);
|
|
156
|
-
|
|
155
|
+
|
|
157
156
|
if (enabled === "true") {
|
|
158
157
|
winston.verbose('ActivityArchiver enabled');
|
|
159
158
|
} else {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const axios = require("axios").default;
|
|
2
|
+
var winston = require('../../config/winston');
|
|
3
|
+
|
|
4
|
+
// Exchange and routing key are fixed by chat21-server (WEBHOOK_MODE=queue).
|
|
5
|
+
const CHAT21_WEBHOOK_EXCHANGE = 'chat21.webhooks';
|
|
6
|
+
const CHAT21_WEBHOOK_ROUTING_KEY = 'chat21.webhook.event';
|
|
7
|
+
|
|
8
|
+
const port = process.env.PORT || '3000';
|
|
9
|
+
const apiEndpoint = process.env.API_ENDPOINT || ("http://localhost:" + port);
|
|
10
|
+
|
|
11
|
+
function positiveNumber(value, defaultValue) {
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
return Number.isFinite(n) && n > 0 ? n : defaultValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const config = {
|
|
17
|
+
queueName: process.env.CHAT21_WEBHOOK_QUEUE_NAME || 'chat21-webhooks',
|
|
18
|
+
// Max messages per second sent to the webhook API. 0 = unlimited.
|
|
19
|
+
rateLimit: positiveNumber(process.env.CHAT21_WEBHOOK_RATE_LIMIT, 0),
|
|
20
|
+
prefetch: positiveNumber(process.env.CHAT21_WEBHOOK_PREFETCH, 10),
|
|
21
|
+
apiUrl: process.env.CHAT21_WEBHOOK_API_URL || (apiEndpoint + "/chat21/requests"),
|
|
22
|
+
timeoutMs: positiveNumber(process.env.CHAT21_WEBHOOK_TIMEOUT_MS, 30000),
|
|
23
|
+
requeueDelayMs: positiveNumber(process.env.CHAT21_WEBHOOK_REQUEUE_DELAY_MS, 1000)
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Spreads the executions so that at most ratePerSecond functions start every second.
|
|
28
|
+
*/
|
|
29
|
+
function createRateLimiter(ratePerSecond) {
|
|
30
|
+
const interval = ratePerSecond > 0 ? 1000 / ratePerSecond : 0;
|
|
31
|
+
let nextSlot = 0;
|
|
32
|
+
return {
|
|
33
|
+
schedule: function (fn) {
|
|
34
|
+
if (!interval) {
|
|
35
|
+
return fn();
|
|
36
|
+
}
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const at = Math.max(now, nextSlot);
|
|
39
|
+
nextSlot = at + interval;
|
|
40
|
+
setTimeout(fn, at - now);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Posts the chat21 event to the webhook API, the same call chat21-server makes in http mode.
|
|
47
|
+
* Resolves to 'ack' or 'requeue'.
|
|
48
|
+
*/
|
|
49
|
+
function handleMessage(msg, options) {
|
|
50
|
+
const opts = options || {};
|
|
51
|
+
const apiUrl = opts.apiUrl || config.apiUrl;
|
|
52
|
+
const timeoutMs = opts.timeoutMs || config.timeoutMs;
|
|
53
|
+
const post = opts.post || function (url, body) {
|
|
54
|
+
return axios.post(url, body, { timeout: timeoutMs, headers: { 'Content-Type': 'application/json' } });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let body;
|
|
58
|
+
try {
|
|
59
|
+
body = JSON.parse(msg.content.toString());
|
|
60
|
+
} catch (e) {
|
|
61
|
+
winston.error("[Chat21WebhookWorker] Discarding malformed message", e);
|
|
62
|
+
return Promise.resolve('ack');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return post(apiUrl, body).then(function (response) {
|
|
66
|
+
winston.debug("[Chat21WebhookWorker] event " + body.event_type + " delivered, status " + response.status);
|
|
67
|
+
return 'ack';
|
|
68
|
+
}).catch(function (err) {
|
|
69
|
+
const response = err && err.response;
|
|
70
|
+
if (!response) {
|
|
71
|
+
winston.error("[Chat21WebhookWorker] webhook API unreachable, requeueing event " + body.event_type + ": " + (err && err.message));
|
|
72
|
+
return 'requeue';
|
|
73
|
+
}
|
|
74
|
+
if (response.status < 500) {
|
|
75
|
+
// e.g. 400 'Not a support message': the event is not meant for tiledesk-server.
|
|
76
|
+
winston.debug("[Chat21WebhookWorker] event " + body.event_type + " rejected with status " + response.status);
|
|
77
|
+
return 'ack';
|
|
78
|
+
}
|
|
79
|
+
if (msg.fields && msg.fields.redelivered) {
|
|
80
|
+
winston.error("[Chat21WebhookWorker] event " + body.event_type + " failed again with status " + response.status + ", discarding it");
|
|
81
|
+
return 'ack';
|
|
82
|
+
}
|
|
83
|
+
winston.warn("[Chat21WebhookWorker] event " + body.event_type + " failed with status " + response.status + ", requeueing it");
|
|
84
|
+
return 'requeue';
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function start(amqpConn, closeOnErr) {
|
|
89
|
+
const limiter = createRateLimiter(config.rateLimit);
|
|
90
|
+
winston.info("[Chat21WebhookWorker] queue: " + config.queueName + " exchange: " + CHAT21_WEBHOOK_EXCHANGE +
|
|
91
|
+
" routing key: " + CHAT21_WEBHOOK_ROUTING_KEY + " api: " + config.apiUrl +
|
|
92
|
+
" rate limit (msg/s): " + (config.rateLimit || "unlimited") + " prefetch: " + config.prefetch);
|
|
93
|
+
|
|
94
|
+
amqpConn.createChannel(function (err, ch) {
|
|
95
|
+
if (closeOnErr(err)) return;
|
|
96
|
+
ch.on("error", function (err) {
|
|
97
|
+
winston.error("[Chat21WebhookWorker] channel error", err);
|
|
98
|
+
});
|
|
99
|
+
ch.on("close", function () {
|
|
100
|
+
winston.info("[Chat21WebhookWorker] channel closed");
|
|
101
|
+
});
|
|
102
|
+
ch.prefetch(config.prefetch);
|
|
103
|
+
ch.assertExchange(CHAT21_WEBHOOK_EXCHANGE, 'topic', { durable: true });
|
|
104
|
+
ch.assertQueue(config.queueName, { durable: true }, function (err, _ok) {
|
|
105
|
+
if (closeOnErr(err)) return;
|
|
106
|
+
ch.bindQueue(_ok.queue, CHAT21_WEBHOOK_EXCHANGE, CHAT21_WEBHOOK_ROUTING_KEY, {}, function (err3, oka) {
|
|
107
|
+
winston.info("Queue bind: " + _ok.queue + " err: " + err3 + " key: " + CHAT21_WEBHOOK_ROUTING_KEY);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
ch.consume(_ok.queue, function (msg) {
|
|
111
|
+
if (!msg) return;
|
|
112
|
+
limiter.schedule(function () {
|
|
113
|
+
handleMessage(msg).then(function (outcome) {
|
|
114
|
+
if (outcome === 'ack') {
|
|
115
|
+
safely(function () { ch.ack(msg); });
|
|
116
|
+
} else {
|
|
117
|
+
setTimeout(function () {
|
|
118
|
+
safely(function () { ch.nack(msg, false, true); });
|
|
119
|
+
}, config.requeueDelayMs);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}, { noAck: false });
|
|
124
|
+
winston.info("[Chat21WebhookWorker] Worker is started");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ack/nack throw if the channel has been closed meanwhile: the broker requeues the message anyway.
|
|
130
|
+
function safely(fn) {
|
|
131
|
+
try {
|
|
132
|
+
fn();
|
|
133
|
+
} catch (e) {
|
|
134
|
+
winston.warn("[Chat21WebhookWorker] ack/nack failed", e.message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
CHAT21_WEBHOOK_EXCHANGE: CHAT21_WEBHOOK_EXCHANGE,
|
|
140
|
+
CHAT21_WEBHOOK_ROUTING_KEY: CHAT21_WEBHOOK_ROUTING_KEY,
|
|
141
|
+
config: config,
|
|
142
|
+
createRateLimiter: createRateLimiter,
|
|
143
|
+
handleMessage: handleMessage,
|
|
144
|
+
start: start
|
|
145
|
+
};
|