@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
function parseList(value) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return value.split(",").map(function (t) { return t.trim(); }).filter(function (t) { return t.length > 0; });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Selects the routing keys the worker binds to, based on INCLUDE_QUEUE / EXCLUDE_QUEUE
|
|
11
|
+
* (comma separated routing keys, mutually exclusive).
|
|
12
|
+
* Returns { enabled, disabled, unknown, filtered }.
|
|
13
|
+
*/
|
|
14
|
+
function resolveTopics(allTopics, includeValue, excludeValue) {
|
|
15
|
+
const include = parseList(includeValue);
|
|
16
|
+
const exclude = parseList(excludeValue);
|
|
17
|
+
|
|
18
|
+
if (include.length > 0 && exclude.length > 0) {
|
|
19
|
+
throw new Error("INCLUDE_QUEUE and EXCLUDE_QUEUE are mutually exclusive. Set only one of them.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const selection = include.length > 0 ? include : exclude;
|
|
23
|
+
const unknown = selection.filter(function (t) { return allTopics.indexOf(t) === -1; });
|
|
24
|
+
|
|
25
|
+
let enabled;
|
|
26
|
+
if (include.length > 0) {
|
|
27
|
+
enabled = allTopics.filter(function (t) { return include.indexOf(t) !== -1; });
|
|
28
|
+
} else {
|
|
29
|
+
enabled = allTopics.filter(function (t) { return exclude.indexOf(t) === -1; });
|
|
30
|
+
}
|
|
31
|
+
const disabled = allTopics.filter(function (t) { return enabled.indexOf(t) === -1; });
|
|
32
|
+
|
|
33
|
+
return { enabled: enabled, disabled: disabled, unknown: unknown, filtered: selection.length > 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { parseList: parseList, resolveTopics: resolveTopics };
|
|
@@ -8,6 +8,8 @@ const botEvent = require('../../event/botEvent');
|
|
|
8
8
|
const authEvent = require('../../event/authEvent');
|
|
9
9
|
const kbEvent = require('../../event/kbEvent');
|
|
10
10
|
const projectUserUpdateContextUtil = require('../../utils/projectUserUpdateContextUtil');
|
|
11
|
+
const queueTopicsFilter = require('./queueTopicsFilter');
|
|
12
|
+
const chat21WebhookWorker = require('./chat21WebhookWorker');
|
|
11
13
|
// https://elements.heroku.com/addons/cloudamqp
|
|
12
14
|
// https://gist.github.com/carlhoerberg/006b01ac17a0a94859ba#file-reconnect-js
|
|
13
15
|
// http://www.rabbitmq.com/tutorials/tutorial-one-javascript.html
|
|
@@ -36,6 +38,63 @@ var exchange = process.env.QUEUE_EXCHANGE_TOPIC || 'amq.topic';
|
|
|
36
38
|
var queueName = process.env.QUEUE_NAME || 'jobs';
|
|
37
39
|
winston.info("Durable queue: " + durable + " Persistent queue: " + persistent + " Exchange topic: " + exchange+ " Queue name: " + queueName);
|
|
38
40
|
|
|
41
|
+
// Routing keys bound to the jobs queue (QUEUE_NAME) on the tiledesk exchange.
|
|
42
|
+
const JOBS_TOPICS = [
|
|
43
|
+
"request_create",
|
|
44
|
+
"request_update_preflight",
|
|
45
|
+
"request_participants_update",
|
|
46
|
+
"request_participants_join",
|
|
47
|
+
"request_participants_leave",
|
|
48
|
+
"request_assigned",
|
|
49
|
+
"request_update",
|
|
50
|
+
"request_close",
|
|
51
|
+
"request_close_extended",
|
|
52
|
+
"request_workingStatus_update",
|
|
53
|
+
"message_create",
|
|
54
|
+
"project_user_update",
|
|
55
|
+
"project_user_invite",
|
|
56
|
+
"project_user_invite_pending",
|
|
57
|
+
"project_user_delete",
|
|
58
|
+
"faqbot_update",
|
|
59
|
+
"faqbot_created",
|
|
60
|
+
"faqbot_deleted",
|
|
61
|
+
"faqbot_publish",
|
|
62
|
+
"kb_namespace_create",
|
|
63
|
+
"kb_namespace_delete",
|
|
64
|
+
"kb_contents_add",
|
|
65
|
+
"kb_contents_delete",
|
|
66
|
+
"kb_content_delete",
|
|
67
|
+
"lead_create",
|
|
68
|
+
"lead_update",
|
|
69
|
+
"lead_fullname_email_update",
|
|
70
|
+
"request_snapshot_update"
|
|
71
|
+
];
|
|
72
|
+
// Consumed from its own queue (CHAT21_WEBHOOK_QUEUE_NAME), see chat21WebhookWorker.js.
|
|
73
|
+
const CHAT21_WEBHOOK_TOPIC = chat21WebhookWorker.CHAT21_WEBHOOK_ROUTING_KEY;
|
|
74
|
+
const ALL_WORKER_TOPICS = JOBS_TOPICS.concat([CHAT21_WEBHOOK_TOPIC]);
|
|
75
|
+
|
|
76
|
+
var topicsSelection = { enabled: ALL_WORKER_TOPICS, disabled: [], unknown: [], filtered: false };
|
|
77
|
+
var enabledJobsTopics = JOBS_TOPICS;
|
|
78
|
+
var disabledJobsTopics = [];
|
|
79
|
+
var chat21WebhookEnabled = true;
|
|
80
|
+
|
|
81
|
+
// Applies INCLUDE_QUEUE / EXCLUDE_QUEUE so that dedicated worker replicas can be deployed.
|
|
82
|
+
// Replicas with different filters on the jobs topics must use different QUEUE_NAME values:
|
|
83
|
+
// bindings belong to the queue, so replicas sharing a queue share its bindings.
|
|
84
|
+
function resolveWorkerTopics() {
|
|
85
|
+
topicsSelection = queueTopicsFilter.resolveTopics(ALL_WORKER_TOPICS, process.env.INCLUDE_QUEUE, process.env.EXCLUDE_QUEUE);
|
|
86
|
+
if (topicsSelection.unknown.length > 0) {
|
|
87
|
+
winston.warn("INCLUDE_QUEUE/EXCLUDE_QUEUE contain unknown routing keys: " + topicsSelection.unknown.join(","));
|
|
88
|
+
}
|
|
89
|
+
enabledJobsTopics = JOBS_TOPICS.filter(function(t) { return topicsSelection.enabled.indexOf(t) !== -1; });
|
|
90
|
+
disabledJobsTopics = JOBS_TOPICS.filter(function(t) { return topicsSelection.enabled.indexOf(t) === -1; });
|
|
91
|
+
chat21WebhookEnabled = topicsSelection.enabled.indexOf(CHAT21_WEBHOOK_TOPIC) !== -1;
|
|
92
|
+
winston.info("Worker topics enabled: " + topicsSelection.enabled.join(","));
|
|
93
|
+
if (topicsSelection.disabled.length > 0) {
|
|
94
|
+
winston.info("Worker topics disabled: " + topicsSelection.disabled.join(","));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
39
98
|
|
|
40
99
|
function start() {
|
|
41
100
|
amqp.connect(url, function(err, conn) {
|
|
@@ -122,6 +181,13 @@ function publish(exchange, routingKey, content) {
|
|
|
122
181
|
// A worker that acks messages only if processed succesfully
|
|
123
182
|
// var channel;
|
|
124
183
|
function startWorker() {
|
|
184
|
+
if (chat21WebhookEnabled) {
|
|
185
|
+
chat21WebhookWorker.start(amqpConn, closeOnErr);
|
|
186
|
+
}
|
|
187
|
+
if (enabledJobsTopics.length === 0) {
|
|
188
|
+
winston.info("Queue " + queueName + " not consumed: no jobs topic enabled by INCLUDE_QUEUE/EXCLUDE_QUEUE");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
125
191
|
amqpConn.createChannel(function(err, ch) {
|
|
126
192
|
if (closeOnErr(err)) return;
|
|
127
193
|
ch.on("error", function(err) {
|
|
@@ -139,143 +205,21 @@ function startWorker() {
|
|
|
139
205
|
ch.assertQueue(queueName, { durable: durable }, function(err, _ok) {
|
|
140
206
|
// ch.assertQueue("jobs", { durable: true }, function(err, _ok) {
|
|
141
207
|
if (closeOnErr(err)) return;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
winston.info("
|
|
145
|
-
});
|
|
146
|
-
ch.bindQueue(_ok.queue, exchange, "request_update_preflight", {}, function(err3, oka) {
|
|
147
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_update_preflight");
|
|
208
|
+
enabledJobsTopics.forEach(function(topic) {
|
|
209
|
+
ch.bindQueue(_ok.queue, exchange, topic, {}, function(err3, oka) {
|
|
210
|
+
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: "+topic);
|
|
148
211
|
winston.info("Data queue", oka)
|
|
149
|
-
|
|
150
|
-
ch.bindQueue(_ok.queue, exchange, "request_participants_update", {}, function(err3, oka) {
|
|
151
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_update");
|
|
152
|
-
winston.info("Data queue", oka)
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
ch.bindQueue(_ok.queue, exchange, "request_participants_join", {}, function(err3, oka) {
|
|
156
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_join");
|
|
157
|
-
winston.info("Data queue", oka)
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
ch.bindQueue(_ok.queue, exchange, "request_participants_leave", {}, function(err3, oka) {
|
|
161
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_leave");
|
|
162
|
-
winston.info("Data queue", oka)
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
ch.bindQueue(_ok.queue, exchange, "request_assigned", {}, function(err3, oka) {
|
|
166
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_assigned");
|
|
167
|
-
winston.info("Data queue", oka)
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
ch.bindQueue(_ok.queue, exchange, "request_update", {}, function(err3, oka) {
|
|
171
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_update");
|
|
172
|
-
winston.info("Data queue", oka)
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
ch.bindQueue(_ok.queue, exchange, "request_close", {}, function(err3, oka) {
|
|
176
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_close");
|
|
177
|
-
winston.info("Data queue", oka)
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
ch.bindQueue(_ok.queue, exchange, "request_close_extended", {}, function(err3, oka) {
|
|
181
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_close_extended");
|
|
182
|
-
winston.info("Data queue", oka)
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
ch.bindQueue(_ok.queue, exchange, "request_workingStatus_update", {}, function(err3, oka) {
|
|
186
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_workingStatus_update");
|
|
187
|
-
winston.info("Data queue", oka)
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
ch.bindQueue(_ok.queue, exchange, "message_create", {}, function(err3, oka) {
|
|
191
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: message_create");
|
|
192
|
-
winston.info("Data queue", oka)
|
|
193
|
-
});
|
|
194
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_update", {}, function(err3, oka) {
|
|
195
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_update");
|
|
196
|
-
winston.info("Data queue", oka)
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_invite", {}, function(err3, oka) {
|
|
200
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_invite");
|
|
201
|
-
winston.info("Data queue", oka)
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_invite_pending", {}, function(err3, oka) {
|
|
205
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_invite_pending");
|
|
206
|
-
winston.info("Data queue", oka)
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
ch.bindQueue(_ok.queue, exchange, "project_user_delete", {}, function(err3, oka) {
|
|
210
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_delete");
|
|
211
|
-
winston.info("Data queue", oka)
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_update", {}, function(err3, oka) {
|
|
215
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_update");
|
|
216
|
-
winston.info("Data queue", oka)
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_created", {}, function(err3, oka) {
|
|
220
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_created");
|
|
221
|
-
winston.info("Data queue", oka)
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_deleted", {}, function(err3, oka) {
|
|
225
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_deleted");
|
|
226
|
-
winston.info("Data queue", oka)
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
ch.bindQueue(_ok.queue, exchange, "faqbot_publish", {}, function(err3, oka) {
|
|
230
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_publish");
|
|
231
|
-
winston.info("Data queue", oka)
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
ch.bindQueue(_ok.queue, exchange, "kb_namespace_create", {}, function(err3, oka) {
|
|
235
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_namespace_create");
|
|
236
|
-
winston.info("Data queue", oka)
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
ch.bindQueue(_ok.queue, exchange, "kb_namespace_delete", {}, function(err3, oka) {
|
|
240
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_namespace_delete");
|
|
241
|
-
winston.info("Data queue", oka)
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
ch.bindQueue(_ok.queue, exchange, "kb_contents_add", {}, function(err3, oka) {
|
|
245
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_contents_add");
|
|
246
|
-
winston.info("Data queue", oka)
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
ch.bindQueue(_ok.queue, exchange, "kb_contents_delete", {}, function(err3, oka) {
|
|
250
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_contents_delete");
|
|
251
|
-
winston.info("Data queue", oka)
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
ch.bindQueue(_ok.queue, exchange, "kb_content_delete", {}, function(err3, oka) {
|
|
255
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_content_delete");
|
|
256
|
-
winston.info("Data queue", oka)
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
ch.bindQueue(_ok.queue, exchange, "lead_create", {}, function(err3, oka) {
|
|
260
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_create");
|
|
261
|
-
winston.info("Data queue", oka)
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
ch.bindQueue(_ok.queue, exchange, "lead_update", {}, function(err3, oka) {
|
|
265
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_update");
|
|
266
|
-
winston.info("Data queue", oka)
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
ch.bindQueue(_ok.queue, exchange, "lead_fullname_email_update", {}, function(err3, oka) {
|
|
270
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_fullname_email_update");
|
|
271
|
-
winston.info("Data queue", oka)
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
ch.bindQueue(_ok.queue, exchange, "request_snapshot_update", {}, function(err3, oka) {
|
|
275
|
-
winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_snapshot_update");
|
|
276
|
-
winston.info("Data queue", oka)
|
|
212
|
+
});
|
|
277
213
|
});
|
|
278
214
|
|
|
215
|
+
if (topicsSelection.filtered) {
|
|
216
|
+
// Remove bindings left on the queue by a previous configuration.
|
|
217
|
+
disabledJobsTopics.forEach(function(topic) {
|
|
218
|
+
ch.unbindQueue(_ok.queue, exchange, topic, {}, function(err3) {
|
|
219
|
+
winston.info("Queue unbind: "+_ok.queue+ " err: "+err3+ " key: "+topic);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
279
223
|
|
|
280
224
|
ch.consume(queueName, processMsg, { noAck: false });
|
|
281
225
|
winston.info("Worker is started");
|
|
@@ -804,6 +748,12 @@ function listen() {
|
|
|
804
748
|
}
|
|
805
749
|
|
|
806
750
|
if (process.env.QUEUE_ENABLED === "true") {
|
|
751
|
+
try {
|
|
752
|
+
resolveWorkerTopics();
|
|
753
|
+
} catch (e) {
|
|
754
|
+
winston.error("Invalid worker queue configuration: " + e.message);
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
807
757
|
requestEvent.queueEnabled = true;
|
|
808
758
|
messageEvent.queueEnabled = true;
|
|
809
759
|
authEvent.queueEnabled = true;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// During the test the env variable is set to test
|
|
2
|
+
process.env.NODE_ENV = 'test';
|
|
3
|
+
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const queueTopicsFilter = require('../queueTopicsFilter');
|
|
6
|
+
const chat21WebhookWorker = require('../chat21WebhookWorker');
|
|
7
|
+
|
|
8
|
+
const TOPICS = ['request_create', 'message_create', 'chat21.webhook.event'];
|
|
9
|
+
|
|
10
|
+
function makeMsg(body, redelivered) {
|
|
11
|
+
return {
|
|
12
|
+
content: Buffer.from(typeof body === 'string' ? body : JSON.stringify(body)),
|
|
13
|
+
fields: { redelivered: !!redelivered }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function httpError(status) {
|
|
18
|
+
const err = new Error('Request failed with status code ' + status);
|
|
19
|
+
err.response = { status: status };
|
|
20
|
+
return err;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('queue worker', function () {
|
|
24
|
+
|
|
25
|
+
describe('INCLUDE_QUEUE / EXCLUDE_QUEUE', function () {
|
|
26
|
+
it('enables every topic when no filter is set', function () {
|
|
27
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, undefined, '');
|
|
28
|
+
expect(res.enabled).to.deep.equal(TOPICS);
|
|
29
|
+
expect(res.disabled).to.deep.equal([]);
|
|
30
|
+
expect(res.filtered).to.equal(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('INCLUDE_QUEUE enables only the listed topics', function () {
|
|
34
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, ' chat21.webhook.event ', undefined);
|
|
35
|
+
expect(res.enabled).to.deep.equal(['chat21.webhook.event']);
|
|
36
|
+
expect(res.disabled).to.deep.equal(['request_create', 'message_create']);
|
|
37
|
+
expect(res.filtered).to.equal(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('EXCLUDE_QUEUE disables the listed topics', function () {
|
|
41
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, undefined, 'chat21.webhook.event,request_create');
|
|
42
|
+
expect(res.enabled).to.deep.equal(['message_create']);
|
|
43
|
+
expect(res.disabled).to.deep.equal(['request_create', 'chat21.webhook.event']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('reports unknown routing keys', function () {
|
|
47
|
+
const res = queueTopicsFilter.resolveTopics(TOPICS, 'message_create,not_a_topic', undefined);
|
|
48
|
+
expect(res.enabled).to.deep.equal(['message_create']);
|
|
49
|
+
expect(res.unknown).to.deep.equal(['not_a_topic']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects INCLUDE_QUEUE and EXCLUDE_QUEUE set together', function () {
|
|
53
|
+
expect(function () {
|
|
54
|
+
queueTopicsFilter.resolveTopics(TOPICS, 'message_create', 'request_create');
|
|
55
|
+
}).to.throw(/mutually exclusive/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('chat21 webhook event handling', function () {
|
|
60
|
+
const event = { event_type: 'message-sent', app_id: 'tilechat', data: { text: 'hello' } };
|
|
61
|
+
|
|
62
|
+
it('posts the event body to the chat21 webhook API and acks', async function () {
|
|
63
|
+
const calls = [];
|
|
64
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event), {
|
|
65
|
+
apiUrl: 'http://tiledesk/chat21/requests',
|
|
66
|
+
post: function (url, body) {
|
|
67
|
+
calls.push({ url: url, body: body });
|
|
68
|
+
return Promise.resolve({ status: 200 });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
expect(outcome).to.equal('ack');
|
|
72
|
+
expect(calls).to.deep.equal([{ url: 'http://tiledesk/chat21/requests', body: event }]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('acks events rejected with a 4xx (e.g. not a support message)', async function () {
|
|
76
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event), {
|
|
77
|
+
post: function () { return Promise.reject(httpError(400)); }
|
|
78
|
+
});
|
|
79
|
+
expect(outcome).to.equal('ack');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('requeues on 5xx the first time and discards on redelivery', async function () {
|
|
83
|
+
const post = function () { return Promise.reject(httpError(500)); };
|
|
84
|
+
expect(await chat21WebhookWorker.handleMessage(makeMsg(event, false), { post: post })).to.equal('requeue');
|
|
85
|
+
expect(await chat21WebhookWorker.handleMessage(makeMsg(event, true), { post: post })).to.equal('ack');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('requeues when the API is unreachable', async function () {
|
|
89
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event, true), {
|
|
90
|
+
post: function () { return Promise.reject(new Error('connect ECONNREFUSED')); }
|
|
91
|
+
});
|
|
92
|
+
expect(outcome).to.equal('requeue');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('discards malformed messages', async function () {
|
|
96
|
+
let called = false;
|
|
97
|
+
const outcome = await chat21WebhookWorker.handleMessage(makeMsg('not json'), {
|
|
98
|
+
post: function () { called = true; return Promise.resolve({ status: 200 }); }
|
|
99
|
+
});
|
|
100
|
+
expect(outcome).to.equal('ack');
|
|
101
|
+
expect(called).to.equal(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('rate limiter', function () {
|
|
106
|
+
it('runs immediately when unlimited', function () {
|
|
107
|
+
let runs = 0;
|
|
108
|
+
const limiter = chat21WebhookWorker.createRateLimiter(0);
|
|
109
|
+
for (let i = 0; i < 5; i++) limiter.schedule(function () { runs++; });
|
|
110
|
+
expect(runs).to.equal(5);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('spreads executions according to the rate', function (done) {
|
|
114
|
+
const starts = [];
|
|
115
|
+
const limiter = chat21WebhookWorker.createRateLimiter(20); // one every 50ms
|
|
116
|
+
const begin = Date.now();
|
|
117
|
+
for (let i = 0; i < 5; i++) {
|
|
118
|
+
limiter.schedule(function () {
|
|
119
|
+
starts.push(Date.now() - begin);
|
|
120
|
+
if (starts.length === 5) {
|
|
121
|
+
expect(starts[4]).to.be.at.least(190);
|
|
122
|
+
for (let j = 1; j < starts.length; j++) {
|
|
123
|
+
expect(starts[j] - starts[j - 1]).to.be.at.least(40);
|
|
124
|
+
}
|
|
125
|
+
done();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const winston = require('../config/winston');
|
|
3
|
+
const Chat21SyncTrace = require('../models/chat21SyncTrace');
|
|
4
|
+
|
|
5
|
+
const MAX_STRING_LENGTH = 2000;
|
|
6
|
+
const enabled = process.env.CHAT21_SYNC_TRACE_ENABLED === "true";
|
|
7
|
+
const host = os.hostname();
|
|
8
|
+
|
|
9
|
+
if (enabled) {
|
|
10
|
+
winston.info("[Chat21SyncTracer] Chat21 sync tracing enabled");
|
|
11
|
+
// The TTL index must exist even when MONGOOSE_AUTOINDEX is false
|
|
12
|
+
Chat21SyncTrace.createIndexes().catch((err) => {
|
|
13
|
+
winston.error("[Chat21SyncTracer] Error creating indexes: ", err);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function truncate(value) {
|
|
18
|
+
if (typeof value !== 'string' || value.length <= MAX_STRING_LENGTH) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
return value.substring(0, MAX_STRING_LENGTH) + "...[truncated]";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function describeError(err) {
|
|
25
|
+
if (!err) {
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
28
|
+
if (err instanceof Error) {
|
|
29
|
+
return { message: err.message, code: err.code, stack: truncate(err.stack) };
|
|
30
|
+
}
|
|
31
|
+
return describeResponse(err);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Serialized as a string: chat21 responses may contain keys that MongoDB does not accept (e.g. "$" or ".")
|
|
35
|
+
function describeResponse(body) {
|
|
36
|
+
if (body === undefined) {
|
|
37
|
+
return "undefined";
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return truncate(typeof body === 'string' ? body : JSON.stringify(body));
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return truncate(String(body));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function describeRequest(request) {
|
|
47
|
+
if (!request) {
|
|
48
|
+
return request;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
status: request.status,
|
|
52
|
+
participants: Array.isArray(request.participants) ? Array.from(request.participants) : request.participants,
|
|
53
|
+
channelOutbound: request.channelOutbound?.name,
|
|
54
|
+
lead_id: request.lead?.lead_id
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Saves a trace document. Never throws and never blocks the caller.
|
|
60
|
+
* @param {string} event the traced flow, e.g. 'request.close'
|
|
61
|
+
* @param {{request_id: string, id_project: *}} ref
|
|
62
|
+
* @param {string} step
|
|
63
|
+
* @param {'info'|'success'|'error'|'skipped'} outcome
|
|
64
|
+
* @param {object} [data]
|
|
65
|
+
*/
|
|
66
|
+
function trace(event, ref, step, outcome, data) {
|
|
67
|
+
if (!enabled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
Chat21SyncTrace.create({
|
|
72
|
+
request_id: ref?.request_id,
|
|
73
|
+
id_project: ref?.id_project ? String(ref.id_project) : undefined,
|
|
74
|
+
event: event,
|
|
75
|
+
step: step,
|
|
76
|
+
outcome: outcome,
|
|
77
|
+
data: data,
|
|
78
|
+
host: host,
|
|
79
|
+
pid: process.pid
|
|
80
|
+
}).catch((err) => {
|
|
81
|
+
winston.error("[Chat21SyncTracer] Error saving trace " + event + " " + step + ": ", err);
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
winston.error("[Chat21SyncTracer] Error creating trace " + event + " " + step + ": ", err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Traces a chat21 sdk call as <action>.sent, then <action>.response or <action>.error.
|
|
90
|
+
* The returned promise settles exactly like the one returned by callFn.
|
|
91
|
+
*/
|
|
92
|
+
function traceCall(event, ref, action, data, callFn) {
|
|
93
|
+
trace(event, ref, action + '.sent', 'info', data);
|
|
94
|
+
return callFn().then((response) => {
|
|
95
|
+
trace(event, ref, action + '.response', 'success', { ...data, response: describeResponse(response) });
|
|
96
|
+
return response;
|
|
97
|
+
}, (err) => {
|
|
98
|
+
trace(event, ref, action + '.error', 'error', { ...data, error: describeError(err) });
|
|
99
|
+
throw err;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
enabled,
|
|
105
|
+
trace,
|
|
106
|
+
traceCall,
|
|
107
|
+
describeError,
|
|
108
|
+
describeResponse,
|
|
109
|
+
describeRequest
|
|
110
|
+
};
|