@tiledesk/tiledesk-voice-twilio-connector 0.1.28 → 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.
Files changed (49) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +515 -0
  3. package/index.js +7 -1562
  4. package/package.json +23 -21
  5. package/src/app.js +154 -0
  6. package/src/config/index.js +32 -0
  7. package/src/controllers/VoiceController.js +493 -0
  8. package/src/middlewares/httpLogger.js +43 -0
  9. package/src/models/KeyValueStore.js +78 -0
  10. package/src/routes/manageApp.js +298 -0
  11. package/src/routes/voice.js +22 -0
  12. package/src/services/AiService.js +219 -0
  13. package/src/services/AiService.sdk.js +367 -0
  14. package/src/services/IntegrationService.js +74 -0
  15. package/src/services/MessageService.js +139 -0
  16. package/src/services/README_SDK.md +107 -0
  17. package/src/services/SessionService.js +143 -0
  18. package/src/services/SpeechService.js +134 -0
  19. package/src/services/TiledeskMessageBuilder.js +135 -0
  20. package/src/services/TwilioService.js +129 -0
  21. package/src/services/UploadService.js +78 -0
  22. package/src/services/channels/TiledeskChannel.js +268 -0
  23. package/{tiledesk → src/services/channels}/VoiceChannel.js +20 -59
  24. package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
  25. package/src/services/index.js +45 -0
  26. package/src/services/translators/TiledeskTwilioTranslator.js +514 -0
  27. package/src/utils/fileUtils.js +24 -0
  28. package/src/utils/logger.js +32 -0
  29. package/{tiledesk → src/utils}/utils-message.js +6 -21
  30. package/logs/app.log +0 -3082
  31. package/routes/manageApp.js +0 -419
  32. package/tiledesk/KVBaseMongo.js +0 -101
  33. package/tiledesk/TiledeskChannel.js +0 -363
  34. package/tiledesk/TiledeskSubscriptionClient.js +0 -135
  35. package/tiledesk/TiledeskTwilioTranslator.js +0 -707
  36. package/tiledesk/fileUtils.js +0 -55
  37. package/tiledesk/services/AiService.js +0 -230
  38. package/tiledesk/services/IntegrationService.js +0 -81
  39. package/tiledesk/services/UploadService.js +0 -88
  40. /package/{winston.js → src/config/logger.js} +0 -0
  41. /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
  42. /package/{template → src/template}/configure.html +0 -0
  43. /package/{template → src/template}/css/configure.css +0 -0
  44. /package/{template → src/template}/css/error.css +0 -0
  45. /package/{template → src/template}/css/style.css +0 -0
  46. /package/{template → src/template}/error.html +0 -0
  47. /package/{tiledesk → src/utils}/constants.js +0 -0
  48. /package/{tiledesk → src/utils}/errors.js +0 -0
  49. /package/{tiledesk → src/utils}/utils.js +0 -0
@@ -1,707 +0,0 @@
1
- const { v4: uuidv4 } = require("uuid");
2
- const winston = require("../winston");
3
- const xmlbuilder = require("xmlbuilder");
4
- const querystring = require("querystring");
5
-
6
- const utils = require("./utils.js");
7
- const utils_message = require("./utils-message.js");
8
- const MENU_CHOICE = require("./constants.js").MENU_CHOICE;
9
- const WAIT_MESSAGE = require("./constants.js").WAIT_MESSAGE;
10
- const TEXT_MESSAGE = require("./constants.js").TEXT_MESSAGE;
11
- const SETTING_MESSAGE = require('./constants').SETTING_MESSAGE
12
- const CHANNEL_NAME = require('./constants').CHANNEL_NAME
13
- const VOICE_PROVIDER = require('./constants').VOICE_PROVIDER;
14
- const OPENAI_SETTINGS = require('./constants').OPENAI_SETTINGS;
15
- const ELEVENLABS_SETTINGS = require('./constants').ELEVENLABS_SETTINGS;
16
-
17
- const TYPE_ACTION_VXML = require('./constants').TYPE_ACTION_VXML
18
- const TYPE_MESSAGE = require('./constants').TYPE_MESSAGE
19
- const INFO_MESSAGE_TYPE = require('./constants').INFO_MESSAGE_TYPE
20
-
21
- const voiceEventEmitter = require('./services/voiceEventEmitter');
22
-
23
- const { SttError } = require('./errors');
24
-
25
- class TiledeskTwilioTranslator {
26
- /**
27
- * Constructor for TiledeskVXMLTranslator
28
- * const axios = require("axios").default;
29
- * @example
30
- * const { TiledeskVXMLTranslator } = require('tiledesk-vxml-translator');
31
- * const tlr = new TiledeskVXMLTranslator();
32
- *
33
- * @param {Object} config JSON configuration.
34
- */
35
-
36
- constructor(config) {
37
- /*
38
- if (!config.tiledeskChannelMessage) {
39
- throw new Error('config.tiledeskChannelMessage is mandatory');
40
- }
41
- this.tiledeskChannelMessage = config.tiledeskChannelMessage;
42
- */
43
- if (!config.BASE_URL) {
44
- throw new Error("[TiledeskVXMLTranslator] config.APP_ID is mandatory");
45
- }
46
- this.BASE_URL = config.BASE_URL;
47
-
48
- if(config.aiService){
49
- this.aiService = config.aiService
50
- }
51
- if(config.uploadService){
52
- this.uploadService = config.uploadService
53
- }
54
-
55
-
56
- this.log = false;
57
- }
58
-
59
- manageVoiceAttributes(msg, vxmlAttributes){
60
-
61
- winston.debug("[TiledeskVXMLTranslator] manageVoiceAttributes: msg.attributes:", msg.attributes);
62
- if(msg.attributes && msg.attributes.flowAttributes){
63
-
64
- let flowAttributes = msg.attributes.flowAttributes;
65
- Object.keys(vxmlAttributes).forEach((key)=> {
66
- if(flowAttributes[key]){
67
- vxmlAttributes[key] = flowAttributes[key];
68
- }
69
- })
70
-
71
-
72
- //MANAGE VOICE SETTINGS from globals attributes
73
- this.voiceProvider = VOICE_PROVIDER.TWILIO
74
- if(flowAttributes.VOICE_PROVIDER){
75
- this.voiceProvider = flowAttributes.VOICE_PROVIDER
76
- }
77
- vxmlAttributes.VOICE_PROVIDER = this.voiceProvider;
78
-
79
- // IF VOICE_PROVIDER is TWILIO --> default values is on user account twilio settings
80
- // IF VOICE_PROVIDER is OPENAI --> set default values from constants
81
- if(this.voiceProvider === VOICE_PROVIDER.OPENAI){
82
- vxmlAttributes.TTS_VOICE_NAME = flowAttributes.TTS_VOICE_NAME? flowAttributes.TTS_VOICE_NAME : OPENAI_SETTINGS.TTS_VOICE_NAME;
83
- vxmlAttributes.TTS_MODEL = flowAttributes.TTS_MODEL? flowAttributes.TTS_MODEL : OPENAI_SETTINGS.TTS_MODEL;
84
- vxmlAttributes.STT_MODEL = flowAttributes.STT_MODEL? flowAttributes.STT_MODEL : OPENAI_SETTINGS.STT_MODEL;
85
- }
86
-
87
- // IF VOICE_PROVIDER is ELEVENLABS --> default values is on user account twilio settings
88
- // IF VOICE_PROVIDER is ELEVENLABS --> set default values from constants
89
- if(this.voiceProvider === VOICE_PROVIDER.ELEVENLABS){
90
- vxmlAttributes.TTS_VOICE_NAME = flowAttributes.TTS_VOICE_NAME? flowAttributes.TTS_VOICE_NAME : ELEVENLABS_SETTINGS.TTS_VOICE_NAME;
91
- vxmlAttributes.TTS_MODEL = flowAttributes.TTS_MODEL? flowAttributes.TTS_MODEL : ELEVENLABS_SETTINGS.TTS_MODEL;
92
- vxmlAttributes.TTS_VOICE_LANGUAGE = flowAttributes.TTS_VOICE_LANGUAGE? flowAttributes.TTS_VOICE_LANGUAGE : ELEVENLABS_SETTINGS.TTS_VOICE_LANGUAGE;
93
- vxmlAttributes.STT_MODEL = flowAttributes.STT_MODEL? flowAttributes.STT_MODEL : ELEVENLABS_SETTINGS.STT_MODEL;
94
- }
95
-
96
- }
97
-
98
- winston.debug("[TiledeskVXMLTranslator] manageVoiceAttributes: vxmlAttributes returned:", vxmlAttributes);
99
- voiceEventEmitter.emit('saveSettings', vxmlAttributes);
100
-
101
- return vxmlAttributes
102
- }
103
-
104
-
105
-
106
- async toVXML(msg, id, vxmlAttributes, sessionInfo) {
107
-
108
-
109
-
110
- vxmlAttributes.intentName=''
111
- if(msg.attributes.intentName){
112
- vxmlAttributes.intentName = msg.attributes.intentName
113
- }
114
-
115
- vxmlAttributes = this.manageVoiceAttributes(msg, vxmlAttributes)
116
-
117
-
118
- this.user = sessionInfo.user
119
- this.integrations = sessionInfo.integrations
120
-
121
-
122
- const xml = xmlbuilder.create("Response", {});
123
- //const header = this.headerVXML(xml, vxmlAttributes);
124
-
125
-
126
- //MANAGE CLOSE info message
127
- const isInfoSupport = utils_message.messageType(TYPE_MESSAGE.INFO_SUPPORT, msg)
128
- winston.debug("[TiledeskVXMLTranslator] isInfoSupport:"+ isInfoSupport);
129
- if(isInfoSupport && utils_message.infoMessageType(msg) === INFO_MESSAGE_TYPE.CHAT_CLOSED){
130
- const hangUp = this.hangupCall(xml)
131
- return hangUp;
132
- }
133
-
134
-
135
- if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
136
- const commands = msg.attributes.commands;
137
-
138
- const attr = this.setVXMLAttributes(commands, vxmlAttributes)
139
-
140
- /** check for WAIT **/
141
- const isWait = this.checkIfIsWait(msg);
142
- winston.debug("[TiledeskVXMLTranslator] toVXML: isWait:"+ isWait);
143
- if(isWait){
144
- const delayForm = await this.delayVXMLConverter(xml, msg, vxmlAttributes);
145
- return delayForm;
146
- }
147
-
148
- /** check for DTMF FORM **/
149
- const isDtmfForm = this.checkIfIsDTMFForm(msg);
150
- winston.debug("[TiledeskVXMLTranslator] toVXML: isDtmfForm: "+ isDtmfForm);
151
- if(isDtmfForm){
152
- const DTMFForm = await this.dtmfFormVXMLConverter(xml, msg, vxmlAttributes);
153
- return DTMFForm;
154
- }
155
-
156
- /** check for BLIND TRANSFER **/
157
- const isBlindFransfer = this.checkIfIsBlindFransfer(msg);
158
- winston.debug("[TiledeskVXMLTranslator] toVXML: isBlindFransfer: "+ isBlindFransfer);
159
- if(isBlindFransfer){
160
- const blindTransfer = await this.blindTransferVXMLConverter(xml, msg, vxmlAttributes);
161
- return blindTransfer;
162
- }
163
-
164
- /** check for DTMF MENU **/
165
- const isMenu = this.checkIfIsDTMFMenuMessage(msg);
166
- winston.debug("[TiledeskVXMLTranslator] toVXML: isMenu: "+ isMenu);
167
- if(isMenu){
168
- const menu = await this.menuVXMLConverter(xml, msg, vxmlAttributes);
169
- return menu;
170
- }
171
-
172
- /** check for SPEECH FORM **/
173
- const isSpeechForm = this.checkIfIsSpeechFormMessage(msg);
174
- winston.debug("[TiledeskVXMLTranslator] toVXML: isSpeechForm: "+ isSpeechForm);
175
- if(isSpeechForm){
176
- const form = await this.speechFormVXMLConverter(xml, msg, vxmlAttributes);
177
- return form;
178
- }
179
-
180
- /** check for FORM (PlayPrompt action) **/
181
- winston.debug("[TiledeskVXMLTranslator] toVXML: isPrompt: true");
182
- const prompt = await this.playPromptVXMLConverter(xml, msg, vxmlAttributes);
183
- return prompt;
184
-
185
- }
186
-
187
- }
188
-
189
- checkIfIsDTMFMenuMessage(msg) {
190
- const commands = msg.attributes.commands;
191
- let dtmf_element = commands.filter((command) => command.type === SETTING_MESSAGE)
192
- if(dtmf_element && dtmf_element.length > 0 && dtmf_element[0].subType === TYPE_ACTION_VXML.DTMF_MENU){
193
- return true;
194
- }
195
- return false;
196
- }
197
-
198
- checkIfIsDTMFForm(msg){
199
- const commands = msg.attributes.commands;
200
- let dtmf_element = commands.filter((command) => command.type === SETTING_MESSAGE)
201
- if(dtmf_element && dtmf_element.length > 0 && dtmf_element[0].subType === TYPE_ACTION_VXML.DTMF_FORM){
202
- return true;
203
- }
204
- return false;
205
- }
206
-
207
- checkIfIsBlindFransfer(msg){
208
- const commands = msg.attributes.commands;
209
- let dtmf_element = commands.filter((command) => command.type === SETTING_MESSAGE)
210
- if(dtmf_element && dtmf_element.length > 0 && dtmf_element[0].subType === TYPE_ACTION_VXML.BLIND_TRANSFER ){
211
- return true;
212
- }
213
- return false;
214
- }
215
-
216
- checkIfIsSpeechFormMessage(msg){
217
- const commands = msg.attributes.commands;
218
- let dtmf_element = commands.filter((command) => command.type === SETTING_MESSAGE)
219
- if(dtmf_element && dtmf_element.length > 0 && dtmf_element[0].subType === TYPE_ACTION_VXML.SPEECH_FORM ){
220
- return true;
221
- }
222
- return false;
223
- }
224
-
225
- checkIfIsWait(msg){
226
- const commands = msg.attributes.commands;
227
- let wait_element = commands.filter((command) => command.type === WAIT_MESSAGE)
228
- let message_element = commands.filter((command) => command.type === TEXT_MESSAGE)
229
- if(wait_element && wait_element.length === 1 && message_element && message_element.length === 0){
230
- return true;
231
- }
232
- return false;
233
- }
234
-
235
- setVXMLAttributes(commands, attributes){
236
- const settingsCommand = commands.slice(-1)[0];
237
- if(settingsCommand && settingsCommand.settings){
238
- Object.keys(settingsCommand.settings).forEach((key) => attributes[key]= settingsCommand.settings[key])
239
- }
240
- return attributes
241
- }
242
-
243
- toTiledesk(vxmlMessage) {
244
- winston.debug("[TiledeskVXMLTranslator] vxml message: ", vxmlMessage);
245
- }
246
-
247
- getMessageFromTdMessage(msg) {
248
- let text = "";
249
- if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
250
- /*msg.attributes.commands.forEach((command) => {
251
- if(command.type === 'message'){
252
- text= command.message.text
253
- }
254
- })*/
255
- let command = msg.attributes.commands[1];
256
- if (command.type === "message") {
257
- text = command.message.text;
258
- }
259
- }
260
-
261
- return text;
262
- }
263
-
264
- getButtonsFromCommand(command) {
265
- let buttons = [];
266
-
267
- if (command.type === "message" && command.message.attributes && command.message.attributes.attachment && command.message.attributes.attachment.buttons) {
268
- /* FILTER only 'action' button type */
269
- buttons = command.message.attributes.attachment.buttons.filter( (button) => (button.type === "action") );
270
- }
271
- return buttons;
272
- }
273
- getButtonsFromTdMessage(msg) {
274
- let buttons = [];
275
- if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
276
- /*msg.attributes.commands.forEach((command) => {
277
- if(command.type === 'message'){
278
- text= command.message.text
279
- }
280
- })*/
281
- let command = msg.attributes.commands[1];
282
- if (command.type === "message" && command.message.attributes && command.message.attributes.attachment && command.message.attributes.attachment.buttons ) {
283
- buttons = command.message.attributes.attachment.buttons.filter( (button) => (button.type === "action") );
284
- }
285
- }
286
- return buttons;
287
- }
288
-
289
- async hangupCall(rootEle){
290
- rootEle.ele("Hangup").up()
291
-
292
- return rootEle.end({ pretty: true });
293
- }
294
-
295
-
296
- async delayVXMLConverter(rootEle, message, xmlAttributes){
297
- const command = message.attributes.commands[0]
298
-
299
- const prompt = this.promptVXML(rootEle, message, xmlAttributes);
300
-
301
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/nextblock/' + xmlAttributes.callSid).up()
302
-
303
- return rootEle.end({ pretty: true });
304
- }
305
-
306
-
307
- async playPromptVXMLConverter(rootEle, message, xmlAttributes){
308
-
309
- const gather = rootEle.ele("Gather", { input: "speech"})
310
-
311
- const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + '&previousIntentTimestamp='+Date.now();
312
- gather.att("action", this.BASE_URL + '/nextBlock/' + xmlAttributes.callSid + queryUrl)
313
- // gather.att("action", this.BASE_URL + '/speechresult/' + xmlAttributes.callSid + queryUrl)
314
- .att("method", "POST")
315
- .att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
316
- .att('speechTimeout', "auto")
317
- .att("enhanced", "true") // enable enhanced recognition
318
-
319
- const prompt = await this.promptVXML(rootEle, message, xmlAttributes);
320
-
321
- /** fallback se non parla --> redirige alla nextblock */
322
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/nextblock/' + xmlAttributes.callSid + queryUrl).up()
323
- //prompt.ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl +'/nextblock/' + session.connection.calltoken", method: "post", namelist: "usertext session intentName previousIntentTimestamp" });
324
-
325
- return rootEle.end({ pretty: true });
326
-
327
- }
328
-
329
- /** DONE **/
330
- async speechFormVXMLConverter(rootEle, message, xmlAttributes) {
331
-
332
- if(this.voiceProvider === VOICE_PROVIDER.TWILIO){
333
- const gather = rootEle.ele("Gather", { input: "speech"})
334
-
335
- const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
336
- gather.att("action", this.BASE_URL + '/nextBlock/' + xmlAttributes.callSid + queryUrl)
337
- // gather.att("action", this.BASE_URL + '/speechresult/' + xmlAttributes.callSid + queryUrl)
338
- .att("method", "POST")
339
- .att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
340
- .att('speechTimeout', "auto")
341
- .att("enhanced", "true") // enable enhanced recognition
342
-
343
- //if(xmlAttributes && xmlAttributes.noInputTimeout){
344
- // gather.att("timeout", Math.round(xmlAttributes.noInputTimeout/1000) ).up();
345
- //}
346
- if(xmlAttributes && xmlAttributes.incompleteSpeechTimeout){
347
- gather.att("speechTimeout", Math.round(xmlAttributes.incompleteSpeechTimeout/1000) ).up();
348
- }
349
-
350
- const prompt = this.promptVXML(gather, message, xmlAttributes);
351
-
352
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
353
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
354
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
355
- }
356
-
357
- }else{
358
-
359
- const prompt = await this.promptVXML(rootEle, message, xmlAttributes);
360
-
361
- const record = rootEle.ele("Record", { playBeep: "false"})
362
-
363
- let queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
364
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
365
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
366
- queryUrl += '&'+ handleNoInputNoMatchQuery.queryNoInput
367
- }
368
-
369
- record
370
- .att("action", this.BASE_URL + '/record/action/' + xmlAttributes.callSid + queryUrl)
371
- .att("method", "POST")
372
- .att("trim", "trim-silence")
373
- .att("timeout", "2")
374
- .att("recordingStatusCallback", this.BASE_URL + '/record/callback/' + xmlAttributes.callSid + queryUrl)
375
- .att("recordingStatusCallbackMethod", "POST")
376
-
377
- // if(xmlAttributes && xmlAttributes.noInputTimeout){
378
- // record.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
379
- // }
380
-
381
- }
382
-
383
- return rootEle.end({ pretty: true });
384
- }
385
-
386
- /** DONE **/
387
- async menuVXMLConverter(rootEle, message, xmlAttributes) {
388
- const lastMessageCommand = message.attributes.commands.slice(-3)[0];
389
- const options = this.getButtonsFromCommand(lastMessageCommand);
390
-
391
- let menu_options = ''
392
- options.forEach((option) => menu_options += option.value + ':' + option.action.substring(1) + ';')
393
-
394
-
395
- let queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + '&previousIntentTimestamp='+Date.now() + '&menu_options=' + menu_options;
396
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
397
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoMatch){
398
- queryUrl += '&'+ handleNoInputNoMatchQuery.queryNoMatch
399
- }
400
-
401
- const gather = rootEle.ele("Gather", { input: "dtmf"})
402
-
403
-
404
- gather.att("timeout", Math.round(xmlAttributes.noInputTimeout/1000) )
405
- .att("numDigits", "1" )
406
- .att("action", this.BASE_URL + '/menublock/' + xmlAttributes.callSid + queryUrl)
407
- .att("method", "POST")
408
- .att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
409
-
410
- const prompt = await this.promptVXML(gather, message, xmlAttributes);
411
-
412
-
413
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
414
- rootEle.ele("Redirect", {}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
415
- }
416
-
417
- return rootEle.end({ pretty: true });
418
- }
419
-
420
- /** DONE **/
421
- async dtmfFormVXMLConverter(rootEle, message, xmlAttributes) {
422
-
423
- const gather = rootEle.ele("Gather", { input: "dtmf"})
424
-
425
- const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
426
- gather.att("timeout", Math.round(xmlAttributes.noInputTimeout/1000) )
427
- .att("action", this.BASE_URL + '/menublock/' + xmlAttributes.callSid + queryUrl)
428
- .att("method", "POST")
429
- .att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
430
- const settings = await this.optionsVXML(gather, message, xmlAttributes);
431
-
432
- const prompt = this.promptVXML(gather, message, xmlAttributes);
433
-
434
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
435
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
436
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
437
- }
438
-
439
- //.ele('disconnect')
440
- return rootEle.end({ pretty: true });
441
- }
442
-
443
-
444
- async blindTransferVXMLConverter(rootEle, message, xmlAttributes) {
445
- const lastMessageCommand = message.attributes.commands.slice(-3)[0];
446
- const options = this.getButtonsFromCommand(lastMessageCommand);
447
-
448
- const prompt = this.promptVXML(rootEle, message, xmlAttributes);
449
- const transfer = this.transferVXML(rootEle, message, xmlAttributes)
450
-
451
-
452
- return rootEle.end({ pretty: true });
453
- }
454
-
455
- async headerVXML(rootEle, attributes) {
456
- //rootEle.att("xmlns", "http://www.w3.org/2001/vxml");
457
- //rootEle.att("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
458
- //rootEle.att( "xsi:schemaLocation", "http://www.w3.org/2001/vxml http://www.w3.org/TR/2007/REC-voicexml21-20070619/vxml.xsd");
459
- //rootEle.att("version", "2.1");
460
- //rootEle.att("xml:lang", attributes.TTS_VOICE_LANGUAGE);
461
-
462
- rootEle.ele("Parameter", { name: "callSid", value: "'" + attributes.callSid + "'" }).up();
463
- rootEle.ele("Parameter", { name: "intentName", value: "'" + attributes.intentName + "'"}).up();
464
- rootEle.ele("Parameter", { name: "previousIntentTimestamp", value: "'" + Date.now() + "'"}).up();
465
- rootEle.ele("Parameter", { name: "proxyBaseUrl", value: "'" + this.BASE_URL + "'"}).up();
466
- const catchVXML = this.catchVXMLEvent(rootEle, attributes);
467
- return rootEle;
468
- }
469
-
470
- async catchVXMLEvent(rootEle, attributes) {
471
- //rootEle.ele("var", { name: "disconnection_url", expr: "'https://tiledesk-vxml-connector.glitch.me/event/'"}).up()
472
- rootEle.ele("catch", { event: "connection.disconnect" })
473
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl+'/event/' + session.connection.calltoken + '/disconnect'", method: "post", namelist: "intentName previousIntentTimestamp"}).up();
474
- rootEle.ele("catch", { event: "connection.disconnect.hangup" })
475
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl+'/event/' + session.connection.calltoken + '/hangup'", method: "post", namelist: "intentName previousIntentTimestamp"}).up();
476
- rootEle.ele("catch", { event: "error.noresource.asr" })
477
- //.ele("var", { name: "url", expr: "'https://tiledesk-vxml-connector.glitch.me/event" + "/no-resources?callId=" + attributes.callId + "'" }).up()
478
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl+'/event/' + session.connection.calltoken + '/no-resources?event=' + application._event", method: "post", namelist: "intentName previousIntentTimestamp" }).up();
479
- return rootEle;
480
- }
481
-
482
-
483
- async handleNoInputNoMatch(rootEle, msg, attributes){
484
- let intent_noInput = null, intent_noMatch = null
485
- let button_noIput = {}, button_noMatch = {};
486
- let queryNoInput = '', queryNoMatch = ''
487
-
488
- let dtmf_form_element = msg.attributes.commands.find((command) => command.type === SETTING_MESSAGE)
489
- if(!dtmf_form_element){
490
- return;
491
- }
492
-
493
-
494
- if(dtmf_form_element.settings.noInputIntent){
495
- intent_noInput = dtmf_form_element.settings.noInputIntent
496
- button_noIput = {
497
- action: intent_noInput,
498
- value: 'no_input'
499
- }
500
-
501
-
502
- queryNoInput = 'button_action='+button_noIput.action.substring(1);
503
- //rootEle.ele("Redirect", {}, this.BASE_URL + '/handle/' + attributes.callSid + '/no_input'+ queryNoInput)
504
-
505
-
506
- /*element.ele("noinput")
507
- .ele("assign", { name: "menu_choice", expr: "'" + MENU_CHOICE.NO_INPUT + "'", }).up()
508
- .ele("assign", { name: "button", expr: "'" + JSON.stringify(button_noIput) + "'", }).up()
509
- .ele("goto", { next: "#noinput_form" }).up()
510
- .up()
511
-
512
- rootEle.ele("form", { id: "noinput_form" })
513
- .ele("block")
514
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl +'/handle/' + session.connection.calltoken + '/no_input'", method: "post", namelist: "intentName button previousIntentTimestamp"}).up()
515
- .up()
516
- .up();
517
- */
518
-
519
- }
520
-
521
-
522
- if(dtmf_form_element.settings.noMatchIntent){
523
- intent_noMatch = dtmf_form_element.settings.noMatchIntent
524
- button_noMatch = {
525
- action: intent_noMatch,
526
- value: 'no_match'
527
- }
528
-
529
- queryNoMatch = 'button_action='+button_noMatch.action.substring(1); //remove '#' from intentId because is not a valid char for XML lang
530
- //rootEle.ele("Redirect", {}, this.BASE_URL + '/handle/' + attributes.callSid + '/no_match'+ queryNoMatch)
531
-
532
- /*element.ele("nomatch")
533
- .ele("assign", { name: "menu_choice", expr: "'" + MENU_CHOICE.NO_MATCH + "'"}).up()
534
- .ele("assign", { name: "button", expr: "'" + JSON.stringify(button_noMatch) + "'", }).up()
535
- .ele("goto", { next: "#nomatch_form" }).up()
536
- .up();
537
-
538
- rootEle.ele("form", { id: "nomatch_form" })
539
- .ele("block")
540
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl +'/handle/' + session.connection.calltoken + '/no_match'", method: "post", namelist: "intentName button previousIntentTimestamp"}).up()
541
- .up()
542
- .up();
543
- */
544
-
545
- }
546
-
547
-
548
- return { queryNoInput: queryNoInput, queryNoMatch: queryNoMatch}
549
-
550
-
551
- }
552
-
553
- /* create PROMPT section */
554
- async promptVXML(rootEle, msg, attributes) {
555
- //let promt = rootEle.ele("prompt", {bargein: attributes.bargein});
556
-
557
- if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
558
- let commands = msg.attributes.commands;
559
- let i = 0;
560
- await new Promise((resolve, reject) => {
561
- const that = this
562
- async function execute(command) {
563
- if (command.type === "message") {
564
- //case type: TEXT
565
- if(command.message.type === 'text'){
566
- let text = utils.markdownToTwilioSpeech(command.message.text);
567
- if(that.voiceProvider !== VOICE_PROVIDER.TWILIO){
568
- let voiceMessageUrl = await that.generateTTS(text, attributes)
569
- rootEle.ele('Play', {}, voiceMessageUrl )
570
- }else{
571
- rootEle.ele("Say", { voice: attributes.TTS_VOICE_NAME, language: attributes.TTS_VOICE_LANGUAGE }, text);
572
- }
573
-
574
- }
575
- //case type: FRAME
576
- if(command.message.type === 'frame' && command.message.metadata.src !== ""){
577
- if(command.message.text != ''){
578
- rootEle.ele("Say", { voice: attributes.TTS_VOICE_NAME, language: attributes.TTS_VOICE_LANGUAGE }, text);
579
- }
580
- rootEle.ele('Play', {}, command.message.metadata.src )
581
- }
582
- } else if (command.type === "wait" && command.time !== 0) {
583
- rootEle.ele("Pause", { length: command.time/1000 }).up();
584
- }
585
-
586
- i += 1;
587
- if (i < commands.length) {
588
- execute(commands[i]);
589
- } else {
590
- resolve(true);
591
- }
592
- }
593
- execute(commands[0]); //START render first message
594
- });
595
- }
596
- return rootEle;
597
- }
598
-
599
- async optionsVXML(rootEle, msg, attributes){
600
-
601
- if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
602
- let commands = msg.attributes.commands;
603
- let dtmf_element = commands.find((command) => command.type === SETTING_MESSAGE)
604
-
605
- if(dtmf_element.settings.minDigits){
606
- attributes.maxDigits = dtmf_element.settings.minDigits
607
- }
608
- if(dtmf_element.settings.maxDigits){
609
- attributes.maxDigits = dtmf_element.settings.maxDigits
610
- rootEle.att('numDigits', attributes.maxDigits)
611
- }
612
- if(dtmf_element.settings.terminators){
613
- attributes.terminators = dtmf_element.settings.terminators
614
- rootEle.att('finishOnKey', attributes.terminators)
615
- }
616
- }
617
- return rootEle
618
- }
619
-
620
- async transferVXML(rootEle, msg, attributes){
621
- const lastCommand = msg.attributes.commands.slice(-1)[0];
622
-
623
- const transfer = rootEle.ele("Dial");
624
-
625
- let queryUrl = '?intentName='+ querystring.encode(attributes.intentName) + '&previousIntentTimestamp='+Date.now()
626
- /* <-- transfer error --> */
627
- if(lastCommand.settings && lastCommand.settings.falseIntent){
628
- queryUrl += '&' + 'button_success=' + attributes.trueIntent.substring(1)
629
-
630
- }
631
-
632
- /* <!-- trasfer OK --> */
633
- if(lastCommand.settings && lastCommand.settings.trueIntent){
634
- queryUrl += '&' + 'button_failure=' + attributes.falseIntent.substring(1)
635
- }
636
-
637
- if(lastCommand.settings && lastCommand.settings.transferTo){
638
- const regexOnlyNumber = new RegExp(/^.*\d.*$/gm)
639
- if(lastCommand.settings.transferTo.match(regexOnlyNumber)){
640
- const number = transfer.ele('Number', {}, lastCommand.settings.transferTo)
641
- number.att('statusCallbackEvent', 'initiated ringing answered completed')
642
- .att('statusCallback', this.BASE_URL + '/event/' + attributes.callSid + '/transfer?' + queryUrl )
643
- .att('statusCallbackMethod', 'POST')
644
- }else{
645
- transfer.att('Sip', {},"'sip:"+ lastCommand.settings.transferTo + attributes.uriTransferParameters+"'")
646
- }
647
-
648
- }
649
-
650
- return transfer.up()
651
- }
652
-
653
- async generateTTS(text, attributes){
654
- let audioData = null;
655
- try {
656
- switch(this.voiceProvider){
657
- case VOICE_PROVIDER.OPENAI:
658
- let GPT_KEY = this.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
659
- audioData = await this.aiService.textToSpeech(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, GPT_KEY)
660
- break;
661
- case VOICE_PROVIDER.ELEVENLABS:
662
- let ELEVENLABS_APIKEY = this.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
663
- audioData = await this.aiService.textToSpeechElevenLabs(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, attributes.TTS_VOICE_LANGUAGE, ELEVENLABS_APIKEY)
664
- break;
665
- default:
666
- throw new SttError('TTS_FAILED', 'Unsupported voice provider: ' + this.voiceProvider);
667
- }
668
-
669
- if (!audioData) {
670
- throw new SttError('TTS_FAILED', 'TTS returned no audio data');
671
- }
672
-
673
- let fileUrl = await this.uploadService.upload(attributes.callSid, audioData, this.user)
674
- winston.debug('(voice) Audio Message url captured after TTS -->', fileUrl)
675
- return fileUrl
676
- } catch (error) {
677
- winston.error('(voice) TTS generation error:', error);
678
- switch (error.code) {
679
- case 'TTS_FAILED':
680
- winston.error('(voice) TTS_FAILED:', error.message);
681
- break;
682
- case 'AI_SERVICE_ERROR':
683
- winston.error('(voice) AI_SERVICE_ERROR:', error.message);
684
- break;
685
- case 'UPLOAD_SERVICE_ERROR':
686
- winston.error('(voice) UPLOAD_SERVICE_ERROR:', error.message);
687
- break;
688
- default:
689
- throw new SttError('TTS_FAILED', 'TTS generation failed: ' + error.message);
690
- }
691
- }
692
-
693
- }
694
-
695
-
696
- async jsonToVxmlConverter(json) {
697
- const root = xmlbuilder.create("vxml");
698
- root.ele("prompt", {}, json.prompt);
699
-
700
- const form = root.ele("form");
701
- json.options.forEach((option) => form.ele("option", {}, option));
702
-
703
- return root.end({ pretty: true });
704
- }
705
- }
706
-
707
- module.exports = { TiledeskTwilioTranslator };