@tiledesk/tiledesk-voice-twilio-connector 0.1.27 → 0.2.0-rc3

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.
Files changed (50) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +44 -0
  3. package/index.js +7 -1529
  4. package/package.json +23 -22
  5. package/src/app.js +146 -0
  6. package/src/config/index.js +32 -0
  7. package/src/controllers/VoiceController.js +488 -0
  8. package/src/controllers/VoiceController.original.js +811 -0
  9. package/src/middlewares/httpLogger.js +31 -0
  10. package/src/models/KeyValueStore.js +78 -0
  11. package/src/routes/manageApp.js +298 -0
  12. package/src/routes/voice.js +22 -0
  13. package/src/services/AiService.js +219 -0
  14. package/src/services/AiService.sdk.js +367 -0
  15. package/src/services/IntegrationService.js +74 -0
  16. package/src/services/MessageService.js +133 -0
  17. package/src/services/README_SDK.md +107 -0
  18. package/src/services/SessionService.js +143 -0
  19. package/src/services/SpeechService.js +134 -0
  20. package/src/services/TiledeskMessageBuilder.js +135 -0
  21. package/src/services/TwilioService.js +122 -0
  22. package/src/services/UploadService.js +78 -0
  23. package/src/services/channels/TiledeskChannel.js +269 -0
  24. package/{tiledesk → src/services/channels}/VoiceChannel.js +17 -56
  25. package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
  26. package/src/services/index.js +45 -0
  27. package/src/services/translators/TiledeskTwilioTranslator.js +509 -0
  28. package/{tiledesk/TiledeskTwilioTranslator.js → src/services/translators/TiledeskTwilioTranslator.original.js} +119 -202
  29. package/src/utils/fileUtils.js +24 -0
  30. package/src/utils/logger.js +32 -0
  31. package/{tiledesk → src/utils}/utils-message.js +6 -21
  32. package/logs/app.log +0 -3082
  33. package/routes/manageApp.js +0 -419
  34. package/tiledesk/KVBaseMongo.js +0 -101
  35. package/tiledesk/TiledeskChannel.js +0 -363
  36. package/tiledesk/TiledeskSubscriptionClient.js +0 -135
  37. package/tiledesk/fileUtils.js +0 -55
  38. package/tiledesk/services/AiService.js +0 -230
  39. package/tiledesk/services/IntegrationService.js +0 -81
  40. package/tiledesk/services/UploadService.js +0 -88
  41. /package/{winston.js → src/config/logger.js} +0 -0
  42. /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
  43. /package/{template → src/template}/configure.html +0 -0
  44. /package/{template → src/template}/css/configure.css +0 -0
  45. /package/{template → src/template}/css/error.css +0 -0
  46. /package/{template → src/template}/css/style.css +0 -0
  47. /package/{template → src/template}/error.html +0 -0
  48. /package/{tiledesk → src/utils}/constants.js +0 -0
  49. /package/{tiledesk → src/utils}/errors.js +0 -0
  50. /package/{tiledesk → src/utils}/utils.js +0 -0
@@ -0,0 +1,269 @@
1
+ const axios = require("axios").default;
2
+ const jwt = require("jsonwebtoken");
3
+
4
+ const utils = require('../../utils/utils-message.js');
5
+ const { TYPE_MESSAGE, CHANNEL_NAME } = require('../../utils/constants');
6
+
7
+ const winston = require("../../utils/logger");
8
+ const voiceEventEmitter = require('../voiceEventEmitter');
9
+
10
+ class TiledeskChannel {
11
+
12
+
13
+ constructor(config) {
14
+
15
+ if (!config) {
16
+ throw new Error("[TiledeskChannel] config is mandatory");
17
+ }
18
+ if (!config.API_URL) {
19
+ throw new Error("[TiledeskChannel] config.API_URL is mandatory");
20
+ }
21
+ if (!config.redis_client) {
22
+ throw new Error("[TiledeskChannel] config.redis_client is mandatory");
23
+ }
24
+
25
+ this.log = config.log || false;
26
+ this.API_URL = config.API_URL;
27
+ this.redis_client = config.redis_client;
28
+ }
29
+
30
+
31
+ async signIn(user_id, settings) {
32
+ // ani = calling phone number
33
+
34
+ winston.debug('[TiledeskChannel] sigIn settings', settings)
35
+
36
+ let payload = {
37
+ _id: CHANNEL_NAME + '-' + user_id,
38
+ firstname: user_id,
39
+ lastname: "",
40
+ phone: user_id,
41
+ sub: "userexternal",
42
+ aud: "https://tiledesk.com/subscriptions/" + settings.subscriptionId,
43
+ };
44
+ let customToken = jwt.sign(payload, settings.secret);
45
+
46
+ try {
47
+ const response = await axios({
48
+ url: this.API_URL + "/auth/signInWithCustomToken",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ Authorization: "JWT " + customToken,
52
+ },
53
+ data: {},
54
+ method: "POST",
55
+ });
56
+
57
+ if (!response.data) {
58
+ return null;
59
+ }
60
+ //response.data.token = await this.fixToken(response.data.token);
61
+ let token = await this.fixToken(response.data.token);
62
+
63
+ let data = {
64
+ token: token,
65
+ _id: response.data.user._id
66
+ };
67
+
68
+ return data;
69
+ } catch (err) {
70
+ winston.error("[TiledeskChannel] sign in error: ", err.response);
71
+ return null;
72
+ }
73
+ }
74
+
75
+ async generateConversation(ani, callId, project_id){
76
+ return "support-group-" + project_id + "-" + ani + "-" + CHANNEL_NAME + "-" + callId;
77
+ }
78
+
79
+ async getConversation(ani, callId, token, project_id) {
80
+ let new_request_id = await this.generateConversation(ani, callId, project_id)
81
+
82
+ try {
83
+ const response = await axios({
84
+ url: this.API_URL + "/" + project_id + "/requests/me?channel=" + CHANNEL_NAME,
85
+ //url: this.API_URL + "/" + project_id + "/requests/me",
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ Authorization: token,
89
+ },
90
+ method: "GET",
91
+ });
92
+
93
+ let request_id;
94
+ if (response.data.requests[0]) {
95
+ request_id = response.data.requests[0].request_id;
96
+ winston.debug("[TiledeskChannel] use already opened conversation: ", request_id);
97
+ } else {
98
+ request_id = new_request_id;
99
+ winston.debug("[TiledeskChannel] use new conversation: ", request_id);
100
+ }
101
+ return request_id;
102
+ } catch (err) {
103
+ winston.error("[TiledeskChannel] get requests error: ", err);
104
+ return null;
105
+ }
106
+ }
107
+
108
+ async getDepartments(token, project_id) {
109
+
110
+ try {
111
+ const response = await axios({
112
+ url: this.API_URL + "/" + project_id + "/departments/allstatus",
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'Authorization': token
116
+ },
117
+ method: 'GET'
118
+ });
119
+ winston.debug("[TiledeskChannel] get departments response.data: ", response.data);
120
+ return response.data;
121
+ } catch (err) {
122
+ winston.error("[TiledeskChannel] get departments error");
123
+ throw err;
124
+ }
125
+ }
126
+
127
+ async send(tiledeskMessage, token, conversation_id, project_id) {
128
+
129
+ try {
130
+ const response = await axios({
131
+ url: this.API_URL + `/${project_id}/requests/${conversation_id}/messages`,
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'Authorization': token
135
+ },
136
+ data: tiledeskMessage,
137
+ method: 'POST'
138
+ });
139
+ winston.debug("[TiledeskChannel] send message response: ", response.data);
140
+ return response.data;
141
+ } catch (err) {
142
+ winston.error("[TiledeskChannel] send message: ", err.response?.data);
143
+ return null;
144
+ }
145
+
146
+ }
147
+
148
+
149
+ /** ADD MESSAGE TO REDIS QUEUE **/
150
+ async addMessageToQueue(message){
151
+
152
+ /*SKIP INFO MESSAGES*/
153
+ if(utils.messageType(TYPE_MESSAGE.INFO, message)){
154
+ winston.debug("> SKIPPING INFO message: " + JSON.stringify(message) );
155
+ return false;
156
+ }
157
+
158
+ /*SKIP CURRENT USER MESSAGES*/
159
+ if (message.sender.indexOf(CHANNEL_NAME) > -1) {
160
+ winston.debug("> SKIPPING ECHO message: " + JSON.stringify(message) );
161
+ return false;
162
+ }
163
+
164
+ winston.debug("> SAVE message TO QUEUE: " + JSON.stringify(message) );
165
+ const conversation_id = message.recipient;
166
+ const queueKey = `tiledesk:queue:${conversation_id}`;
167
+
168
+ // Use pipeline for atomic push + expire (single round-trip)
169
+ await this.redis_client
170
+ .multi()
171
+ .rPush(queueKey, JSON.stringify(message))
172
+ .expire(queueKey, 86400)
173
+ .exec();
174
+
175
+ // Emit event for real-time subscribers
176
+ voiceEventEmitter.emit(`tiledesk:conversation:${conversation_id}`, message);
177
+
178
+ return true;
179
+ }
180
+
181
+
182
+ /** GET MESSAGES FROM REDIS QUEUE LIST **/
183
+ async getMessagesFromQueue(conversation_id){
184
+ const queueKey = `tiledesk:queue:${conversation_id}`;
185
+ const queue = await this.redis_client.lRange(queueKey, 0, -1);
186
+
187
+ if (!queue || queue.length === 0) {
188
+ return [];
189
+ }
190
+
191
+ return queue.map(item => JSON.parse(item));
192
+ }
193
+
194
+ /** PUBLISH MESSAGE TO REDIS TOPIC **/
195
+ async publishMessageToTopic(message){
196
+
197
+ /*SKIP INFO MESSAGES*/
198
+ if(utils.messageType(TYPE_MESSAGE.INFO, message)){
199
+ winston.debug("> SKIPPING INFO message");
200
+ return;
201
+ }
202
+
203
+ /*SKIP CURRENT USER MESSAGES*/
204
+ if (message.sender.indexOf("vxml") > -1) {
205
+ winston.debug("> SKIPPING ECHO message");
206
+ return;
207
+ }
208
+
209
+ voiceEventEmitter.emit(`tiledesk:conversation:${message.recipient}`, message);
210
+ }
211
+
212
+ /** SUBSCRIBE TO REDIS TOPIC */
213
+ async subscribeToTopic(conversation_id){
214
+ const topic = `tiledesk:conversation:${conversation_id}`;
215
+ // console.log("subscribeToTopic: " + topic);
216
+
217
+ return new Promise((resolve, reject) => {
218
+ voiceEventEmitter.once(topic, (message) => {
219
+ resolve(message);
220
+ });
221
+ });
222
+ }
223
+
224
+ /** REMOVE MESSAGE FROM REDIS QUEUE LIST (removes first message - FIFO) **/
225
+ async removeMessageFromQueue(conversation_id, message_id){
226
+ const queueKey = `tiledesk:queue:${conversation_id}`;
227
+ // Use lPop for FIFO queue - removes and returns first element
228
+ await this.redis_client.lPop(queueKey);
229
+ }
230
+
231
+ /** CLEAR QUEUE FROM REDIS **/
232
+ async clearQueue(conversation_id){
233
+ if(conversation_id){
234
+ await this.redis_client.del(`tiledesk:queue:${conversation_id}`);
235
+ }
236
+ }
237
+
238
+
239
+
240
+
241
+ async generateWaitTdMessage(ani, delayTime){
242
+
243
+
244
+ return {
245
+ text: '',
246
+ senderFullname: ani,
247
+ attributes: {
248
+ commands:[
249
+ { type: 'wait', time: delayTime}
250
+ ]
251
+ },
252
+ channel: { name: CHANNEL_NAME }
253
+ }
254
+ }
255
+
256
+
257
+ async fixToken(token) {
258
+ let index = token.lastIndexOf("JWT ");
259
+ if (index != -1) {
260
+ let new_token = token.substring(index + 4);
261
+ return "JWT " + new_token;
262
+ } else {
263
+ return "JWT " + token;
264
+ }
265
+ }
266
+
267
+ }
268
+
269
+ module.exports = { TiledeskChannel };
@@ -1,12 +1,7 @@
1
1
  require('dotenv').config();
2
- const axios = require("axios").default;
3
- const jwt = require("jsonwebtoken");
4
- const { v4: uuidv4 } = require("uuid");
5
- const { promisify } = require('util');
2
+ const winston = require("../../utils/logger");
6
3
 
7
- const winston = require("../winston");
8
-
9
- const voiceEventEmitter = require('./services/voiceEventEmitter');
4
+ const voiceEventEmitter = require('../voiceEventEmitter');
10
5
 
11
6
  class VoiceChannel {
12
7
 
@@ -68,50 +63,23 @@ class VoiceChannel {
68
63
 
69
64
  /** ADD POOLING DELAY INDEX TO REDIS **/
70
65
  async saveDelayIndexForCallId(callId){
71
-
72
- //get value for current callId
73
- const index = await this.redis_client.get('tiledesk:voice:'+callId + ':delayIndex');
74
- if(index){
75
- //increment
76
- const delayIndex = (+index) +1
77
- //save new index to redis
78
- await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', delayIndex, {'EX': 86400});
79
- return;
80
- }
81
- //if index is not present: set to default (0)
82
- await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', 0, {'EX': 86400});
66
+ const key = `tiledesk:voice:${callId}:delayIndex`;
67
+ // Use INCR for atomic increment, initializes to 1 if key doesn't exist
68
+ await this.redis_client.incr(key);
69
+ await this.redis_client.expire(key, 86400);
83
70
  }
84
71
 
85
72
  /** RESET INDEX INTO REDIS DATA FOR CURRENT CALLID **/
86
73
  async clearDelayTimeForCallId(callId){
87
-
88
- //get value for current callId
89
- const index = await this.redis_client.get('tiledesk:voice:'+callId + ':delayIndex');
90
- winston.debug('clearDelayTimeForCallId: index -->'+index)
91
- if(index){
92
- //set index to default (0)
93
- await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', 0, {'EX': 86400});
94
- return;
95
- }
96
- //if index is not present: set to default (0)
74
+ // Always reset to default (0) regardless of current state
97
75
  await this.redis_client.set('tiledesk:voice:'+callId + ':delayIndex', 0, {'EX': 86400});
98
76
  }
99
77
 
100
78
 
101
79
  async saveSettingsForCallId(attributes, callId){
102
-
103
80
  winston.debug('[VoiceChannel] saveSettingsForCallId: attributes -->', attributes)
104
-
105
- const index = await this.redis_client.get('tiledesk:voice:'+callId + ':attributes');
106
- winston.debug('[VoiceChannel] saveSettingsForCallId: attributes found -->'+index)
107
- if(index){
108
- //set index to default (0)
109
- await this.redis_client.set('tiledesk:voice:'+callId + ':attributes', JSON.stringify(attributes), {'EX': 86400});
110
- return;
111
- }
112
- //if index is not present: set to default (0)
81
+ // Always save/overwrite settings
113
82
  await this.redis_client.set('tiledesk:voice:'+callId + ':attributes', JSON.stringify(attributes), {'EX': 86400});
114
-
115
83
  }
116
84
 
117
85
 
@@ -145,22 +113,15 @@ class VoiceChannel {
145
113
 
146
114
 
147
115
  async deleteCallKeys(callSid) {
148
- const pattern = `tiledesk:voice:${callSid}:*`;
149
- let cursor = 0;
150
-
151
- do {
152
- const reply = await this.redis_client.scan(cursor, {
153
- MATCH: pattern,
154
- COUNT: 100
155
- });
156
-
157
- cursor = reply.cursor;
158
- const keys = reply.keys;
159
-
160
- if (keys.length > 0) {
161
- await this.redis_client.del(keys);
162
- }
163
- } while (cursor !== 0);
116
+ // Use known key patterns instead of SCAN for better performance
117
+ const keys = [
118
+ `tiledesk:voice:${callSid}:session`,
119
+ `tiledesk:voice:${callSid}:attributes`,
120
+ `tiledesk:voice:${callSid}:delayIndex`
121
+ ];
122
+
123
+ // Use unlink for non-blocking delete
124
+ await this.redis_client.unlink(keys);
164
125
  }
165
126
 
166
127
 
@@ -0,0 +1,78 @@
1
+ const axios = require("axios").default;
2
+ const winston = require('../../utils/logger')
3
+
4
+ class TiledeskSubscriptionClient {
5
+
6
+ /**
7
+ * Constructor for TiledeskSubscriptionClient
8
+ *
9
+ * @example
10
+ * const { TiledeskSubscriptionClient } = require('tiledesk-subscription-client');
11
+ * const tdClient = new TiledeskSubscriptionClient({API_URL: tiledeskApiUrl, token: jwt_token, log: log});
12
+ * @param {Object} config JSON configuration.
13
+ * @param {string} config.API_URL Mandatory. The Tiledesk api url.
14
+ * @param {string} config.token Optional. Token required for authentication.
15
+ * @param {boolean} config.log Optional. If true HTTP requests are logged.
16
+ */
17
+ constructor(config) {
18
+ if (!config) {
19
+ throw new Error('[TiledeskSubscriptionClient] config is mandatory');
20
+ }
21
+
22
+ if (!config.API_URL) {
23
+ throw new Error('[TiledeskSubscriptionClient] config.API_URL is mandatory');
24
+ }
25
+
26
+ this.project_id = config.project_id
27
+ this.API_URL = config.API_URL;
28
+ this.token = config.token;
29
+ this.config = config;
30
+ this.log = false;
31
+ if (config.log) {
32
+ this.log = config.log;
33
+ }
34
+
35
+ }
36
+
37
+ async subscribe(subscription_info) {
38
+ const URL = this.API_URL + `/${this.project_id}/subscriptions`;
39
+ try {
40
+ const response = await axios({
41
+ url: URL,
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ 'Authorization': this.token
45
+ },
46
+ data: subscription_info,
47
+ method: 'POST'
48
+ });
49
+ winston.debug("[TiledeskSubscriptionClient] Subscribed");
50
+ return response.data;
51
+ } catch (err) {
52
+ throw err;
53
+ }
54
+ }
55
+
56
+ async unsubscribe(subscriptionId) {
57
+ const URL = this.API_URL + `/${this.project_id}/subscriptions/${subscriptionId}`;
58
+ try {
59
+ const response = await axios({
60
+ url: URL,
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ 'Authorization': this.token
64
+ },
65
+ method: 'DELETE'
66
+ });
67
+ winston.debug("[TiledeskSubscriptionClient] Unsubscribed");
68
+ return response.data;
69
+ } catch (err) {
70
+ throw err;
71
+ }
72
+ }
73
+
74
+
75
+
76
+ }
77
+
78
+ module.exports = { TiledeskSubscriptionClient }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Services Index
3
+ *
4
+ * Central export point for all business logic services.
5
+ * Import from this file to access all services in one place.
6
+ */
7
+
8
+ const { SessionService } = require('./SessionService');
9
+ const { MessageService } = require('./MessageService');
10
+ const { SpeechService } = require('./SpeechService');
11
+ const { TwilioService } = require('./TwilioService');
12
+ const { TiledeskMessageBuilder } = require('./TiledeskMessageBuilder');
13
+
14
+ // Channel services
15
+ const { TiledeskChannel } = require('./channels/TiledeskChannel');
16
+ const { VoiceChannel } = require('./channels/VoiceChannel');
17
+
18
+ // Translator services
19
+ const { TiledeskTwilioTranslator } = require('./translators/TiledeskTwilioTranslator');
20
+
21
+ // Integration services
22
+ const { IntegrationService } = require('./IntegrationService');
23
+ const { UploadService } = require('./UploadService');
24
+ const { AiService } = require('./AiService');
25
+
26
+ module.exports = {
27
+ // Core business services
28
+ SessionService,
29
+ MessageService,
30
+ SpeechService,
31
+ TwilioService,
32
+ TiledeskMessageBuilder,
33
+
34
+ // Channel services
35
+ TiledeskChannel,
36
+ VoiceChannel,
37
+
38
+ // Translator services
39
+ TiledeskTwilioTranslator,
40
+
41
+ // Integration services
42
+ IntegrationService,
43
+ UploadService,
44
+ AiService
45
+ };