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

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.
@@ -1,3 +1,4 @@
1
+ const { twiml } = require('twilio');
1
2
  const logger = require('../utils/logger');
2
3
 
3
4
  /**
@@ -44,7 +45,7 @@ class TwilioService {
44
45
  method: 'POST'
45
46
  });
46
47
 
47
- logger.debug(`[TwilioService] Call ${callSid} redirected to ${url}`);
48
+ // logger.debug(`[TwilioService] Call ${callSid} redirected to ${url}`);
48
49
  return true;
49
50
  } catch (error) {
50
51
  logger.error(`[TwilioService] Error redirecting call ${callSid}:`, error);
@@ -77,7 +78,7 @@ class TwilioService {
77
78
  const flowAttrs = tiledeskMessage.attributes?.flowAttributes;
78
79
  const callSid = flowAttrs?.CallSid;
79
80
 
80
- if (!callSid || lastCallSidVerb[callSid] !== 'play') {
81
+ if (!callSid || lastCallSidVerb[callSid] !== 'play_loop') {
81
82
  return false;
82
83
  }
83
84
 
@@ -92,6 +93,12 @@ class TwilioService {
92
93
 
93
94
  return this.redirectCall(callSid, redirectUrl, settings);
94
95
  }
96
+
97
+ generateRedirectTwiML(callSid, intentName) {
98
+ const response = new twiml.VoiceResponse();
99
+ response.redirect({ method: 'POST' }, this.buildNextBlockUrl(callSid, intentName));
100
+ return response.toString();
101
+ }
95
102
 
96
103
  /**
97
104
  * Get call information.
@@ -26,7 +26,7 @@ class UploadService {
26
26
  }
27
27
 
28
28
  async upload(id, file, user) {
29
- winston.debug(`[UploadService] upload for id ${id} and user ${user._id}`);
29
+ // logger.debug(`[UploadService] upload for id ${id} and user ${user._id}`);
30
30
 
31
31
  const tempFilePath = path.join(os.tmpdir(), `speech_${user._id}_${id}.wav`);
32
32
 
@@ -4,14 +4,14 @@ const jwt = require("jsonwebtoken");
4
4
  const utils = require('../../utils/utils-message.js');
5
5
  const { TYPE_MESSAGE, CHANNEL_NAME } = require('../../utils/constants');
6
6
 
7
- const winston = require("../../utils/logger");
7
+ const logger = require("../../utils/logger");
8
8
  const voiceEventEmitter = require('../voiceEventEmitter');
9
9
 
10
10
  class TiledeskChannel {
11
11
 
12
12
 
13
13
  constructor(config) {
14
-
14
+
15
15
  if (!config) {
16
16
  throw new Error("[TiledeskChannel] config is mandatory");
17
17
  }
@@ -21,18 +21,18 @@ class TiledeskChannel {
21
21
  if (!config.redis_client) {
22
22
  throw new Error("[TiledeskChannel] config.redis_client is mandatory");
23
23
  }
24
-
24
+
25
25
  this.log = config.log || false;
26
26
  this.API_URL = config.API_URL;
27
27
  this.redis_client = config.redis_client;
28
28
  }
29
-
29
+
30
30
 
31
31
  async signIn(user_id, settings) {
32
32
  // ani = calling phone number
33
33
 
34
- winston.debug('[TiledeskChannel] sigIn settings', settings)
35
-
34
+ // logger.debug('[TiledeskChannel] sigIn settings', settings)
35
+
36
36
  let payload = {
37
37
  _id: CHANNEL_NAME + '-' + user_id,
38
38
  firstname: user_id,
@@ -71,8 +71,8 @@ class TiledeskChannel {
71
71
  return null;
72
72
  }
73
73
  }
74
-
75
- async generateConversation(ani, callId, project_id){
74
+
75
+ async generateConversation(ani, callId, project_id) {
76
76
  return "support-group-" + project_id + "-" + ani + "-" + CHANNEL_NAME + "-" + callId;
77
77
  }
78
78
 
@@ -93,10 +93,10 @@ class TiledeskChannel {
93
93
  let request_id;
94
94
  if (response.data.requests[0]) {
95
95
  request_id = response.data.requests[0].request_id;
96
- winston.debug("[TiledeskChannel] use already opened conversation: ", request_id);
96
+ // logger.debug("[TiledeskChannel] use already opened conversation: ", request_id);
97
97
  } else {
98
98
  request_id = new_request_id;
99
- winston.debug("[TiledeskChannel] use new conversation: ", request_id);
99
+ // logger.debug("[TiledeskChannel] use new conversation: ", request_id);
100
100
  }
101
101
  return request_id;
102
102
  } catch (err) {
@@ -104,7 +104,7 @@ class TiledeskChannel {
104
104
  return null;
105
105
  }
106
106
  }
107
-
107
+
108
108
  async getDepartments(token, project_id) {
109
109
 
110
110
  try {
@@ -112,11 +112,11 @@ class TiledeskChannel {
112
112
  url: this.API_URL + "/" + project_id + "/departments/allstatus",
113
113
  headers: {
114
114
  'Content-Type': 'application/json',
115
- 'Authorization': token
115
+ 'Authorization': token
116
116
  },
117
117
  method: 'GET'
118
118
  });
119
- winston.debug("[TiledeskChannel] get departments response.data: ", response.data);
119
+ // logger.debug("[TiledeskChannel] get departments response.data: ", response.data);
120
120
  return response.data;
121
121
  } catch (err) {
122
122
  winston.error("[TiledeskChannel] get departments error");
@@ -136,7 +136,7 @@ class TiledeskChannel {
136
136
  data: tiledeskMessage,
137
137
  method: 'POST'
138
138
  });
139
- winston.debug("[TiledeskChannel] send message response: ", response.data);
139
+ // logger.debug("[TiledeskChannel] send message response: ", response.data);
140
140
  return response.data;
141
141
  } catch (err) {
142
142
  winston.error("[TiledeskChannel] send message: ", err.response?.data);
@@ -144,65 +144,64 @@ class TiledeskChannel {
144
144
  }
145
145
 
146
146
  }
147
-
148
-
147
+
148
+
149
149
  /** ADD MESSAGE TO REDIS QUEUE **/
150
- async addMessageToQueue(message){
151
-
150
+ async addMessageToQueue(message) {
151
+
152
152
  /*SKIP INFO MESSAGES*/
153
- if(utils.messageType(TYPE_MESSAGE.INFO, message)){
154
- winston.debug("> SKIPPING INFO message: " + JSON.stringify(message) );
155
- return false;
153
+ if (utils.messageType(TYPE_MESSAGE.INFO, message)) {
154
+ logger.debug("> SKIPPING INFO message");
155
+ // logger.debug("> SKIPPING INFO message: " + JSON.stringify(message) );
156
+ return false;
156
157
  }
157
158
 
158
159
  /*SKIP CURRENT USER MESSAGES*/
159
160
  if (message.sender.indexOf(CHANNEL_NAME) > -1) {
160
- winston.debug("> SKIPPING ECHO message: " + JSON.stringify(message) );
161
+ logger.debug("> SKIPPING ECHO message");
162
+ // logger.debug("> SKIPPING ECHO message: " + JSON.stringify(message) );
161
163
  return false;
162
164
  }
163
165
 
164
- winston.debug("> SAVE message TO QUEUE: " + JSON.stringify(message) );
166
+ // logger.debug("> SAVE message TO QUEUE: " + JSON.stringify(message) );
165
167
  const conversation_id = message.recipient;
166
168
  const queueKey = `tiledesk:queue:${conversation_id}`;
167
-
169
+
168
170
  // Use pipeline for atomic push + expire (single round-trip)
169
171
  await this.redis_client
170
172
  .multi()
171
173
  .rPush(queueKey, JSON.stringify(message))
172
174
  .expire(queueKey, 86400)
173
175
  .exec();
174
-
175
- // Emit event for real-time subscribers
176
- voiceEventEmitter.emit(`tiledesk:conversation:${conversation_id}`, message);
177
-
176
+
178
177
  return true;
179
178
  }
180
179
 
181
-
180
+
182
181
  /** GET MESSAGES FROM REDIS QUEUE LIST **/
183
- async getMessagesFromQueue(conversation_id){
182
+ async getMessagesFromQueue(conversation_id) {
184
183
  const queueKey = `tiledesk:queue:${conversation_id}`;
185
184
  const queue = await this.redis_client.lRange(queueKey, 0, -1);
186
-
185
+
187
186
  if (!queue || queue.length === 0) {
188
187
  return [];
189
188
  }
190
-
189
+
191
190
  return queue.map(item => JSON.parse(item));
192
191
  }
193
192
 
194
193
  /** PUBLISH MESSAGE TO REDIS TOPIC **/
195
- async publishMessageToTopic(message){
196
-
194
+ async publishMessageToTopic(message) {
195
+
197
196
  /*SKIP INFO MESSAGES*/
198
- if(utils.messageType(TYPE_MESSAGE.INFO, message)){
199
- winston.debug("> SKIPPING INFO message");
197
+ if (utils.messageType(TYPE_MESSAGE.INFO, message)) {
198
+ // logger.debug("> SKIPPING INFO message");
200
199
  return;
201
200
  }
202
201
 
203
202
  /*SKIP CURRENT USER MESSAGES*/
204
203
  if (message.sender.indexOf("vxml") > -1) {
205
- winston.debug("> SKIPPING ECHO message");
204
+ // logger.debug("> SKIPPING ECHO message");
206
205
  return;
207
206
  }
208
207
 
@@ -210,7 +209,7 @@ class TiledeskChannel {
210
209
  }
211
210
 
212
211
  /** SUBSCRIBE TO REDIS TOPIC */
213
- async subscribeToTopic(conversation_id){
212
+ async subscribeToTopic(conversation_id) {
214
213
  const topic = `tiledesk:conversation:${conversation_id}`;
215
214
  // console.log("subscribeToTopic: " + topic);
216
215
 
@@ -220,40 +219,40 @@ class TiledeskChannel {
220
219
  });
221
220
  });
222
221
  }
223
-
222
+
224
223
  /** REMOVE MESSAGE FROM REDIS QUEUE LIST (removes first message - FIFO) **/
225
- async removeMessageFromQueue(conversation_id, message_id){
224
+ async removeMessageFromQueue(conversation_id, message_id) {
226
225
  const queueKey = `tiledesk:queue:${conversation_id}`;
227
226
  // Use lPop for FIFO queue - removes and returns first element
228
227
  await this.redis_client.lPop(queueKey);
229
228
  }
230
-
229
+
231
230
  /** CLEAR QUEUE FROM REDIS **/
232
- async clearQueue(conversation_id){
233
- if(conversation_id){
231
+ async clearQueue(conversation_id) {
232
+ if (conversation_id) {
234
233
  await this.redis_client.del(`tiledesk:queue:${conversation_id}`);
235
234
  }
236
235
  }
237
-
238
-
239
-
240
-
241
- async generateWaitTdMessage(ani, delayTime){
242
-
243
-
236
+
237
+
238
+
239
+
240
+ async generateWaitTdMessage(ani, delayTime) {
241
+
242
+
244
243
  return {
245
244
  text: '',
246
245
  senderFullname: ani,
247
246
  attributes: {
248
- commands:[
249
- { type: 'wait', time: delayTime}
247
+ commands: [
248
+ { type: 'wait', time: delayTime }
250
249
  ]
251
250
  },
252
251
  channel: { name: CHANNEL_NAME }
253
252
  }
254
253
  }
255
-
256
-
254
+
255
+
257
256
  async fixToken(token) {
258
257
  let index = token.lastIndexOf("JWT ");
259
258
  if (index != -1) {
@@ -263,7 +262,7 @@ class TiledeskChannel {
263
262
  return "JWT " + token;
264
263
  }
265
264
  }
266
-
265
+
267
266
  }
268
267
 
269
268
  module.exports = { TiledeskChannel };
@@ -1,5 +1,5 @@
1
1
  require('dotenv').config();
2
- const winston = require("../../utils/logger");
2
+ const logger = require("../../utils/logger");
3
3
 
4
4
  const voiceEventEmitter = require('../voiceEventEmitter');
5
5
 
@@ -45,7 +45,7 @@ class VoiceChannel {
45
45
  listenToVoiceEvents(){
46
46
 
47
47
  voiceEventEmitter.on('saveSettings', async (data) => {
48
- winston.debug('[VoiceChannel] listenToVoiceEvents: saveSettings event received -->', data)
48
+ // logger.debug('[VoiceChannel] listenToVoiceEvents: saveSettings event received -->', data)
49
49
  if(data){
50
50
  await this.saveSettingsForCallId(data, data.callSid);
51
51
  }
@@ -77,7 +77,7 @@ class VoiceChannel {
77
77
 
78
78
 
79
79
  async saveSettingsForCallId(attributes, callId){
80
- winston.debug('[VoiceChannel] saveSettingsForCallId: attributes -->', attributes)
80
+ // logger.debug('[VoiceChannel] saveSettingsForCallId: attributes -->', attributes)
81
81
  // Always save/overwrite settings
82
82
  await this.redis_client.set('tiledesk:voice:'+callId + ':attributes', JSON.stringify(attributes), {'EX': 86400});
83
83
  }
@@ -85,7 +85,7 @@ class VoiceChannel {
85
85
 
86
86
  async getSettingsForCallId(callId){
87
87
  const attributes = await this.redis_client.get('tiledesk:voice:'+callId + ':attributes');
88
- winston.debug('[VoiceChannel] getSettingsForCallId: attributes found -->', attributes, callId)
88
+ // logger.debug('[VoiceChannel] getSettingsForCallId: attributes found -->', attributes, callId)
89
89
  if(attributes){
90
90
  return JSON.parse(attributes)
91
91
  }
@@ -46,7 +46,7 @@ class TiledeskSubscriptionClient {
46
46
  data: subscription_info,
47
47
  method: 'POST'
48
48
  });
49
- winston.debug("[TiledeskSubscriptionClient] Subscribed");
49
+ // logger.debug("[TiledeskSubscriptionClient] Subscribed");
50
50
  return response.data;
51
51
  } catch (err) {
52
52
  throw err;
@@ -64,7 +64,7 @@ class TiledeskSubscriptionClient {
64
64
  },
65
65
  method: 'DELETE'
66
66
  });
67
- winston.debug("[TiledeskSubscriptionClient] Unsubscribed");
67
+ // logger.debug("[TiledeskSubscriptionClient] Unsubscribed");
68
68
  return response.data;
69
69
  } catch (err) {
70
70
  throw err;
@@ -32,7 +32,7 @@ class TiledeskTwilioTranslator {
32
32
  if (!config.BASE_URL) {
33
33
  throw new Error('[TiledeskTwilioTranslator] config.BASE_URL is mandatory');
34
34
  }
35
-
35
+
36
36
  this.BASE_URL = config.BASE_URL;
37
37
  this.aiService = config.aiService;
38
38
  this.uploadService = config.uploadService;
@@ -129,7 +129,7 @@ class TiledeskTwilioTranslator {
129
129
  // Apply provider-specific defaults
130
130
  this._applyProviderDefaults(vxmlAttributes, flowAttrs);
131
131
 
132
- logger.debug('[TiledeskTwilioTranslator] Processed vxmlAttributes:', vxmlAttributes);
132
+ // logger.debug('[TiledeskTwilioTranslator] Processed vxmlAttributes:', vxmlAttributes);
133
133
  voiceEventEmitter.emit('saveSettings', vxmlAttributes);
134
134
 
135
135
  return vxmlAttributes;
@@ -218,13 +218,15 @@ class TiledeskTwilioTranslator {
218
218
 
219
219
  // Handle no input/no match scenarios
220
220
  if (noInputNoMatch?.queryNoInput) {
221
- response.redirect({ method: 'POST' },
221
+ response.redirect({ method: 'POST' },
222
222
  `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
223
223
  }
224
224
  } else {
225
225
  // No barge-in: simple play prompt
226
226
  await this._addPromptElements(response, msg, attrs, sessionInfo, false);
227
- response.redirect({ method: 'POST' }, `${this.BASE_URL}/nextblock/${attrs.callSid}`);
227
+ if (!response.toString().trim().includes('<Play loop="300">')) {
228
+ response.redirect({ method: 'POST' }, `${this.BASE_URL}/nextblock/${attrs.callSid}`);
229
+ }
228
230
  }
229
231
 
230
232
  return response.toString();
@@ -253,7 +255,7 @@ class TiledeskTwilioTranslator {
253
255
  await this._addPromptElements(gather, msg, attrs, sessionInfo);
254
256
 
255
257
  if (noInputNoMatch?.queryNoInput) {
256
- response.redirect({ method: 'POST' },
258
+ response.redirect({ method: 'POST' },
257
259
  `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
258
260
  }
259
261
  } else {
@@ -302,7 +304,7 @@ class TiledeskTwilioTranslator {
302
304
  await this._addPromptElements(gather, msg, attrs, sessionInfo);
303
305
 
304
306
  if (noInputNoMatch?.queryNoInput) {
305
- response.redirect({},
307
+ response.redirect({},
306
308
  `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
307
309
  }
308
310
 
@@ -329,7 +331,7 @@ class TiledeskTwilioTranslator {
329
331
  await this._addPromptElements(gather, msg, attrs, sessionInfo);
330
332
 
331
333
  if (noInputNoMatch?.queryNoInput) {
332
- response.redirect({ method: 'POST' },
334
+ response.redirect({ method: 'POST' },
333
335
  `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
334
336
  }
335
337
 
@@ -388,10 +390,10 @@ class TiledeskTwilioTranslator {
388
390
 
389
391
  async _addMessageElement(element, command, attrs, sessionInfo, bargeIn = false) {
390
392
  const { message } = command;
391
-
393
+
392
394
  if (message.type === 'text') {
393
395
  const text = utils.markdownToTwilioSpeech(message.text);
394
-
396
+
395
397
  if (this.voiceProvider !== VOICE_PROVIDER.TWILIO) {
396
398
  const audioUrl = await this._generateTTS(text, attrs, sessionInfo);
397
399
  if (audioUrl) {
@@ -411,12 +413,15 @@ class TiledeskTwilioTranslator {
411
413
  }
412
414
  } else if (message.type === 'frame' && message.metadata?.src) {
413
415
 
414
- const playOptions = { loop: 0 };
416
+ const playOptions = { loop: 1 };
415
417
  if (bargeIn) {
416
418
  playOptions.bargeIn = true;
419
+ this.lastCallSidVerb[attrs.callSid] = 'play';
420
+ } else {
421
+ playOptions.loop = 300; // Long audio (if the audio last 1 second, it will play for 5 minutes)
422
+ this.lastCallSidVerb[attrs.callSid] = 'play_loop';
417
423
  }
418
424
  element.play(playOptions, message.metadata.src);
419
- this.lastCallSidVerb[attrs.callSid] = 'play';
420
425
  }
421
426
  }
422
427
 
@@ -425,13 +430,13 @@ class TiledeskTwilioTranslator {
425
430
  async _generateTTS(text, attrs, sessionInfo) {
426
431
  try {
427
432
  const audioData = await this._callTTSProvider(text, attrs, sessionInfo);
428
-
433
+
429
434
  if (!audioData) {
430
435
  throw new SttError('TTS_FAILED', 'TTS returned no audio data');
431
436
  }
432
437
 
433
438
  const fileUrl = await this.uploadService.upload(attrs.callSid, audioData, sessionInfo.user);
434
- logger.debug('[TiledeskTwilioTranslator] TTS audio URL:', fileUrl);
439
+ // logger.debug('[TiledeskTwilioTranslator] TTS audio URL:', fileUrl);
435
440
  return fileUrl;
436
441
  } catch (error) {
437
442
  logger.error('[TiledeskTwilioTranslator] TTS generation error:', error);
@@ -479,7 +484,7 @@ class TiledeskTwilioTranslator {
479
484
  }
480
485
 
481
486
  _getButtonsFromMessage(msg) {
482
- const command = msg.attributes?.commands?.find(cmd =>
487
+ const command = msg.attributes?.commands?.find(cmd =>
483
488
  cmd.type === 'message' && cmd.message?.attributes?.attachment?.buttons
484
489
  );
485
490
  const buttons = command?.message?.attributes?.attachment?.buttons || [];
@@ -502,7 +507,7 @@ class TiledeskTwilioTranslator {
502
507
 
503
508
  // Legacy method for backwards compatibility
504
509
  toTiledesk(vxmlMessage) {
505
- logger.debug('[TiledeskTwilioTranslator] toTiledesk:', vxmlMessage);
510
+ // logger.debug('[TiledeskTwilioTranslator] toTiledesk:', vxmlMessage);
506
511
  }
507
512
  }
508
513