@tiledesk/tiledesk-whatsapp-connector 1.0.30 → 1.0.32

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/.env CHANGED
@@ -1,2 +1,2 @@
1
1
  # CUSTOM ENV
2
- NPM_PUBLISH_TOKEN=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
2
+ NPM_PUBLISH_TOKEN=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
package/index.js CHANGED
@@ -31,6 +31,7 @@ const { TemplateManager } = require("./tiledesk/TemplateManager");
31
31
  const { WhatsappLogger } = require("./tiledesk/WhatsappLogger");
32
32
  const Utils = require("./tiledesk/Utils");
33
33
  const whatsappService = require("./tiledesk/WhatsappService");
34
+ const { ensureGlobalSigninSubscription, ensureProjectTestSubscription } = require("./tiledesk/BotTestSubscriptions");
34
35
 
35
36
  // mongo
36
37
  const { KVBaseMongo } = require("./tiledesk/KVBaseMongo");
@@ -560,22 +561,22 @@ router.post("/disconnect", async (req, res) => {
560
561
  // ***************************************
561
562
  // ****** MESSAGING SECTION - START ******
562
563
  // ***************************************
563
- router.post("/tiledesk", async (req, res) => {
564
+ function phoneNumberIdFromTiledeskRecipient(recipient, attributes) {
565
+ if (attributes && attributes.whatsapp_phone_number_id) {
566
+ return attributes.whatsapp_phone_number_id;
567
+ }
568
+ if (!recipient || recipient.indexOf("wab-") === -1) {
569
+ return null;
570
+ }
571
+ return recipient.substring(recipient.lastIndexOf("wab-") + 4, recipient.lastIndexOf("-"));
572
+ }
573
+
574
+ async function handleTiledeskOutbound(req, res, settings) {
564
575
  winston.verbose("(wab) Message received from Tiledesk");
565
576
 
566
577
  let tiledeskChannelMessage = req.body.payload;
567
578
  winston.debug("(wab) tiledeskChannelMessage: ", tiledeskChannelMessage);
568
579
 
569
- let project_id = req.body.payload.id_project;
570
- let recipient = req.body.payload.recipient;
571
- let waba_id = recipient.split('-')[5];
572
-
573
- let settings = await utils.getSettingsByProjectId(project_id);
574
-
575
- if (!settings) {
576
- settings = await utils.getSettings(project_id, waba_id);
577
- }
578
-
579
580
  let wab_token = settings.wab_token;
580
581
 
581
582
  var text = req.body.payload.text;
@@ -758,6 +759,55 @@ router.post("/tiledesk", async (req, res) => {
758
759
  winston.debug("(wab) no command, no text --> skip");
759
760
  return res.status(400).send({ success: false, error: "No command or text specified. Skip message." });
760
761
  }
762
+ }
763
+
764
+ router.post("/tiledesk", async (req, res) => {
765
+ const payload = req.body && req.body.payload;
766
+ if (!payload || !payload.recipient) {
767
+ return res.status(400).send({ success: false, error: "Invalid Tiledesk payload" });
768
+ }
769
+
770
+ const project_id = payload.id_project;
771
+ const waba_id = payload.recipient.split("-")[5];
772
+ let settings = await utils.getSettingsByProjectId(project_id);
773
+ if (!settings) {
774
+ settings = await utils.getSettings(project_id, waba_id);
775
+ }
776
+ if (!settings) {
777
+ winston.warn("(wab) settings not found for project " + project_id);
778
+ return res.sendStatus(200);
779
+ }
780
+
781
+ const phoneNumberId = phoneNumberIdFromTiledeskRecipient(payload.recipient, payload.attributes);
782
+ if (phoneNumberId) {
783
+ const official = await db.get("whatsapp-phone-" + phoneNumberId);
784
+ if (official && official.project_id && String(official.project_id) !== String(project_id)) {
785
+ winston.verbose("(wab) Skip Try on WhatsApp reply on the project subscription");
786
+ return res.sendStatus(200);
787
+ }
788
+ }
789
+
790
+ return handleTiledeskOutbound(req, res, settings);
791
+ });
792
+
793
+ router.post("/tiledesk/test", async (req, res) => {
794
+ const payload = req.body && req.body.payload;
795
+ if (!payload || !payload.recipient) {
796
+ return res.sendStatus(200);
797
+ }
798
+
799
+ const phoneNumberId = phoneNumberIdFromTiledeskRecipient(payload.recipient, payload.attributes);
800
+ if (!phoneNumberId) {
801
+ return res.sendStatus(200);
802
+ }
803
+
804
+ const settings = await db.get("whatsapp-phone-" + phoneNumberId);
805
+ if (!settings || !settings.wab_token) {
806
+ winston.verbose("(wab) No official WhatsApp settings for test reply " + phoneNumberId);
807
+ return res.sendStatus(200);
808
+ }
809
+
810
+ return handleTiledeskOutbound(req, res, settings);
761
811
  });
762
812
 
763
813
  // Endpoint for Whatsapp Business connected via OAuth
@@ -817,6 +867,7 @@ router.post('/webhook', async (req, res) => {
817
867
  db: db,
818
868
  tdChannel: tdChannel,
819
869
  tlr: tlr,
870
+ baseUrl: BASE_URL,
820
871
  });
821
872
  bottester.startBotConversation(req.body, code).then((result) => {
822
873
  winston.verbose("(wab) test conversation started");
@@ -998,6 +1049,7 @@ router.post("/webhook/:project_id", async (req, res) => {
998
1049
  db: db,
999
1050
  tdChannel: tdChannel,
1000
1051
  tlr: tlr,
1052
+ baseUrl: BASE_URL,
1001
1053
  });
1002
1054
  bottester.startBotConversation(req.body, code).then((result) => {
1003
1055
  winston.verbose("(wab) test conversation started");
@@ -1268,6 +1320,22 @@ router.post("/newtest", async (req, res) => {
1268
1320
  let project_id = req.body.project_id;
1269
1321
  let bot_id = req.body.bot_id;
1270
1322
 
1323
+ if (!project_id || !bot_id) {
1324
+ return res.status(400).send({ success: false, message: "project_id and bot_id are required" });
1325
+ }
1326
+
1327
+ if (!redis_client) {
1328
+ return res.status(500).send({ message: "Test it out on Whatsapp not available. Redis not ready." });
1329
+ }
1330
+
1331
+ try {
1332
+ await ensureGlobalSigninSubscription(db, BASE_URL);
1333
+ await ensureProjectTestSubscription(db, project_id, BASE_URL, redis_client);
1334
+ } catch (err) {
1335
+ winston.error("(wab) unable to prepare Try on WhatsApp: " + err);
1336
+ return res.status(500).send({ success: false, message: "Try on WhatsApp is not available" });
1337
+ }
1338
+
1271
1339
  let info = {
1272
1340
  project_id: project_id,
1273
1341
  bot_id: bot_id,
@@ -1276,10 +1344,6 @@ router.post("/newtest", async (req, res) => {
1276
1344
  let short_uid = uuidv4().substring(0, 8);
1277
1345
  let key = "bottest:" + short_uid;
1278
1346
 
1279
- if (!redis_client) {
1280
- return res.status(500).send({ message: "Test it out on Whatsapp not available. Redis not ready." });
1281
- }
1282
-
1283
1347
  await redis_client.set(key, JSON.stringify(info), "EX", 604800);
1284
1348
  redis_client.get(key, (err, value) => {
1285
1349
  if (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-whatsapp-connector",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "Tiledesk Whatsapp connector",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -16,7 +16,7 @@
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
18
  "@tiledesk/tiledesk-client": "^0.10.4",
19
- "@tiledesk/jobs-worker-queued": "^0.0.6",
19
+ "@tiledesk/jobs-worker-queued": "^0.0.8",
20
20
  "app-root-path": "^3.0.0",
21
21
  "axios": "^0.27.2",
22
22
  "body-parser": "^1.20.0",
@@ -0,0 +1,124 @@
1
+ const { v4: uuidv4 } = require("uuid");
2
+ const winston = require("../winston");
3
+
4
+ const BOTTEST_SIGNIN_EVENT = "whatsapp.bottest.signin";
5
+ const WHATSAPP_MESSAGE_EVENT = "message.create.request.channel.whatsapp";
6
+ const BOTTEST_CREATED_BY = "whatsapp-bottest";
7
+
8
+ function isProjectId(value) {
9
+ return typeof value === "string" && /^[a-fA-F0-9]{24}$/.test(value);
10
+ }
11
+
12
+ function subscriptions(kv) {
13
+ if (!kv || !kv.db || typeof kv.db.collection !== "function") {
14
+ throw new Error("WhatsApp storage is not connected");
15
+ }
16
+ return kv.db.collection("subscriptions");
17
+ }
18
+
19
+ async function ensureGlobalSigninSubscription(kv, baseUrl) {
20
+ if (!baseUrl) {
21
+ throw new Error("BASE_URL is required to prepare Try on WhatsApp");
22
+ }
23
+ const collection = subscriptions(kv);
24
+ const query = { event: BOTTEST_SIGNIN_EVENT, id_project: "*", global: true };
25
+ let existing = await collection.findOne(query);
26
+ if (existing) {
27
+ return existing;
28
+ }
29
+
30
+ const now = new Date();
31
+ const doc = {
32
+ event: BOTTEST_SIGNIN_EVENT,
33
+ target: baseUrl + "/tiledesk/test",
34
+ secret: uuidv4(),
35
+ id_project: "*",
36
+ global: true,
37
+ createdBy: BOTTEST_CREATED_BY,
38
+ createdAt: now,
39
+ updatedAt: now
40
+ };
41
+
42
+ try {
43
+ const result = await collection.insertOne(doc);
44
+ doc._id = result.insertedId;
45
+ return doc;
46
+ } catch (err) {
47
+ if (err && err.code === 11000) {
48
+ existing = await collection.findOne(query);
49
+ if (existing) {
50
+ return existing;
51
+ }
52
+ }
53
+ throw err;
54
+ }
55
+ }
56
+
57
+ async function ensureProjectTestSubscription(kv, projectId, baseUrl, redisClient) {
58
+ if (!isProjectId(projectId)) {
59
+ throw new Error("Try on WhatsApp project_id is invalid");
60
+ }
61
+ if (!baseUrl) {
62
+ throw new Error("BASE_URL is required to prepare Try on WhatsApp");
63
+ }
64
+
65
+ const collection = subscriptions(kv);
66
+ const target = baseUrl + "/tiledesk/test";
67
+ const query = { id_project: projectId, event: WHATSAPP_MESSAGE_EVENT, target: target };
68
+ let existing = await collection.findOne(query);
69
+ if (!existing) {
70
+ const now = new Date();
71
+ const doc = {
72
+ event: WHATSAPP_MESSAGE_EVENT,
73
+ target: target,
74
+ secret: uuidv4(),
75
+ id_project: projectId,
76
+ global: false,
77
+ createdBy: BOTTEST_CREATED_BY,
78
+ createdAt: now,
79
+ updatedAt: now
80
+ };
81
+ try {
82
+ const result = await collection.insertOne(doc);
83
+ doc._id = result.insertedId;
84
+ existing = doc;
85
+ } catch (err) {
86
+ if (err && err.code === 11000) {
87
+ existing = await collection.findOne(query);
88
+ } else {
89
+ throw err;
90
+ }
91
+ }
92
+ }
93
+
94
+ await invalidateSubscriptionCache(redisClient, projectId, WHATSAPP_MESSAGE_EVENT);
95
+ return existing;
96
+ }
97
+
98
+ function invalidateSubscriptionCache(redisClient, projectId, event) {
99
+ if (!redisClient || !projectId || !event) {
100
+ return Promise.resolve();
101
+ }
102
+ const key = "cacheman:cachegoose-cache:" + projectId + ":subscriptions:event:" + event;
103
+ return new Promise((resolve) => {
104
+ try {
105
+ redisClient.del(key, function (err) {
106
+ if (err) {
107
+ winston.warn("(wab) unable to refresh subscription cache for " + projectId);
108
+ }
109
+ resolve();
110
+ });
111
+ } catch (err) {
112
+ winston.warn("(wab) unable to refresh subscription cache for " + projectId);
113
+ resolve();
114
+ }
115
+ });
116
+ }
117
+
118
+ module.exports = {
119
+ BOTTEST_SIGNIN_EVENT,
120
+ WHATSAPP_MESSAGE_EVENT,
121
+ isProjectId,
122
+ ensureGlobalSigninSubscription,
123
+ ensureProjectTestSubscription
124
+ };
@@ -1,6 +1,8 @@
1
1
  var JobManager = require("@tiledesk/jobs-worker-queued");
2
2
  var winston = require('../winston');
3
- var jobManager;
3
+
4
+ // One JobManager (and so one AMQP connection) per url and exchange, shared by all the requests
5
+ var jobManagers = {};
4
6
 
5
7
  class Scheduler {
6
8
 
@@ -25,18 +27,22 @@ class Scheduler {
25
27
  this.log = config.log;
26
28
  }
27
29
 
28
- jobManager = new JobManager(this.AMQP_MANAGER_URL, {
29
- debug: false,
30
- topic: this.JOB_TOPIC_EXCHANGE,
31
- exchange: this.JOB_TOPIC_EXCHANGE
32
- });
30
+ let key = this.AMQP_MANAGER_URL + "|" + this.JOB_TOPIC_EXCHANGE;
31
+ if (!jobManagers[key]) {
32
+ jobManagers[key] = new JobManager(this.AMQP_MANAGER_URL, {
33
+ debug: false,
34
+ topic: this.JOB_TOPIC_EXCHANGE,
35
+ exchange: this.JOB_TOPIC_EXCHANGE
36
+ });
37
+ }
38
+ this.jobManager = jobManagers[key];
33
39
  }
34
40
 
35
41
  async goSchedule(mydata) {
36
42
  mydata.date = new Date();
37
43
  try {
38
- winston.debug("(wab) Data arrived to scheduler: ", mydata);
39
- jobManager.publish(mydata);
44
+ winston.verbose("(wab) Scheduling job for transaction_id: " + mydata.transaction_id);
45
+ this.jobManager.publish(mydata);
40
46
  return { success: true, msg: 'scheduling success!' };
41
47
  } catch (err) {
42
48
  winston.error('(wab) scheduling error ', err);
@@ -45,4 +51,4 @@ class Scheduler {
45
51
  }
46
52
  }
47
53
 
48
- module.exports = { Scheduler };
54
+ module.exports = { Scheduler };
@@ -7,6 +7,7 @@ const { TiledeskClient } = require('@tiledesk/tiledesk-client');
7
7
  const { TiledeskWhatsappTranslator } = require('./TiledeskWhatsappTranslator');
8
8
  const { TiledeskChannel } = require('./TiledeskChannel');
9
9
  const Utils = require("./Utils");
10
+ const { ensureGlobalSigninSubscription, ensureProjectTestSubscription, isProjectId } = require("./BotTestSubscriptions");
10
11
 
11
12
  class TiledeskBotTester {
12
13
 
@@ -46,6 +47,7 @@ class TiledeskBotTester {
46
47
  this.db = config.db;
47
48
  this.tdChannel = config.tdChannel;
48
49
  this.tlr = config.tlr;
50
+ this.baseUrl = config.baseUrl;
49
51
 
50
52
  this.utils = new Utils({ db: this.db })
51
53
 
@@ -79,7 +81,7 @@ class TiledeskBotTester {
79
81
  test_info = JSON.parse(value)
80
82
  } catch(err) {
81
83
  winston.error("(wab) Error parsing value: " + value);
82
- reject(err);
84
+ return reject(err);
83
85
  }
84
86
  let message_info = {
85
87
  channel: "whatsapp",
@@ -103,14 +105,29 @@ class TiledeskBotTester {
103
105
 
104
106
  whatsappChannelMessage.text.body = "/start";
105
107
  let tiledeskJsonMessage = await this.tlr.toTiledesk(whatsappChannelMessage, whatsappContact.profile.name);
106
-
107
- if (test_info) {
108
- const response = await this.tdChannel.sendAndAddBot(tiledeskJsonMessage, message_info, test_info.bot_id)
109
- return resolve(response)
110
- } else {
108
+
109
+ if (!test_info || !test_info.bot_id || !isProjectId(test_info.project_id)) {
111
110
  winston.verbose("(wab) No bot selected for test. Exit..")
112
111
  return reject("Test skipped")
113
112
  }
113
+
114
+ try {
115
+ const phoneNumberId = message_info.whatsapp.phone_number_id;
116
+ if (phoneNumberId && this.tdChannel && this.tdChannel.settings) {
117
+ await this.db.set("whatsapp-phone-" + phoneNumberId, this.tdChannel.settings);
118
+ }
119
+ const signinSubscription = await ensureGlobalSigninSubscription(this.db, this.baseUrl);
120
+ await ensureProjectTestSubscription(this.db, test_info.project_id, this.baseUrl, this.redis_client);
121
+ const response = await this.tdChannel.sendAndAddBot(tiledeskJsonMessage, message_info, test_info.bot_id, {
122
+ project_id: test_info.project_id,
123
+ subscriptionId: String(signinSubscription._id),
124
+ secret: signinSubscription.secret
125
+ });
126
+ return resolve(response);
127
+ } catch (prepareErr) {
128
+ winston.error("(wab) [TiledeskBotTester] unable to open test conversation: " + prepareErr);
129
+ return reject(prepareErr);
130
+ }
114
131
 
115
132
  }
116
133
  }
@@ -181,18 +181,23 @@ class TiledeskChannel {
181
181
  * @param {string} bot_id - The bot ID
182
182
  * @returns {Promise<Object>} - The response from the Tiledesk API
183
183
  */
184
- async sendAndAddBot(tiledeskMessage, messageInfo, bot_id) {
185
-
184
+ async sendAndAddBot(tiledeskMessage, messageInfo, bot_id, targetProject) {
185
+
186
+ const projectId = targetProject && targetProject.project_id ? targetProject.project_id : this.settings.project_id;
187
+ const subscriptionId = targetProject && targetProject.subscriptionId ? targetProject.subscriptionId : this.settings.subscriptionId;
188
+ const secret = targetProject && targetProject.secret ? targetProject.secret : this.settings.secret;
189
+
186
190
  let channel;
187
191
  let new_request_id;
188
192
  tiledeskMessage.participants = ["bot_" + bot_id];
189
193
  tiledeskMessage.attributes = {
190
- sourcePage: "whatsapp://&td_draft=true"
194
+ sourcePage: "whatsapp://&td_draft=true",
195
+ whatsapp_bottest: true
191
196
  }
192
197
 
193
198
  if (messageInfo.channel == "whatsapp") {
194
199
  channel = messageInfo.whatsapp;
195
- new_request_id = "support-group-" + this.settings.project_id + "-" + uuidv4().substring(0, 8) + "-wab-" + channel.phone_number_id + "-" + channel.from;
200
+ new_request_id = "support-group-" + projectId + "-" + uuidv4().substring(0, 8) + "-wab-" + channel.phone_number_id + "-" + channel.from;
196
201
  } else {
197
202
  winston.verbose("(wab) [TiledeskChannel] Channel not supported")
198
203
  return null;
@@ -203,10 +208,11 @@ class TiledeskChannel {
203
208
  first_name: channel.firstname,
204
209
  last_name: channel.lastname,
205
210
  sub: 'userexternal',
206
- aud: 'https://tiledesk.com/subscriptions/' + this.settings.subscriptionId
211
+ aud: 'https://tiledesk.com/subscriptions/' + subscriptionId
207
212
  }
208
213
 
209
- var customToken = jwt.sign(payload, this.settings.secret);
214
+ var customToken = jwt.sign(payload, secret);
215
+ const signinBody = targetProject && targetProject.project_id ? { id_project: projectId } : {};
210
216
 
211
217
  return await axios({
212
218
  url: this.API_URL + "/auth/signinWithCustomToken",
@@ -214,7 +220,7 @@ class TiledeskChannel {
214
220
  'Content-Type': 'application/json',
215
221
  'Authorization': 'JWT ' + customToken
216
222
  },
217
- data: {},
223
+ data: signinBody,
218
224
  method: 'POST'
219
225
  }).then((response) => {
220
226
 
@@ -222,7 +228,7 @@ class TiledeskChannel {
222
228
  token = this.fixToken(token);
223
229
 
224
230
  return axios({
225
- url: this.API_URL + `/${this.settings.project_id}/requests/${new_request_id}/messages`,
231
+ url: this.API_URL + `/${projectId}/requests/${new_request_id}/messages`,
226
232
  headers: {
227
233
  'Content-Type': 'application/json',
228
234
  'Authorization': token