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

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,614 +0,0 @@
1
- const { v4: uuidv4 } = require("uuid");
2
- const winston = require("../../utils/logger");
3
- const xmlbuilder = require("xmlbuilder");
4
- const querystring = require("querystring");
5
-
6
- const utils = require("../../utils/utils.js");
7
- const utils_message = require("../../utils/utils-message.js");
8
- const MENU_CHOICE = require("../../utils/constants.js").MENU_CHOICE;
9
- const WAIT_MESSAGE = require("../../utils/constants.js").WAIT_MESSAGE;
10
- const TEXT_MESSAGE = require("../../utils/constants.js").TEXT_MESSAGE;
11
- const SETTING_MESSAGE = require('../../utils/constants').SETTING_MESSAGE
12
- const CHANNEL_NAME = require('../../utils/constants').CHANNEL_NAME
13
- const VOICE_PROVIDER = require('../../utils/constants').VOICE_PROVIDER;
14
- const OPENAI_SETTINGS = require('../../utils/constants').OPENAI_SETTINGS;
15
- const ELEVENLABS_SETTINGS = require('../../utils/constants').ELEVENLABS_SETTINGS;
16
-
17
- const TYPE_ACTION_VXML = require('../../utils/constants').TYPE_ACTION_VXML
18
- const TYPE_MESSAGE = require('../../utils/constants').TYPE_MESSAGE
19
- const INFO_MESSAGE_TYPE = require('../../utils/constants').INFO_MESSAGE_TYPE
20
-
21
- const voiceEventEmitter = require('../voiceEventEmitter');
22
-
23
- const { SttError } = require('../../utils/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
- this.lastCallSidVerb = {};
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, sessionInfo);
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, sessionInfo);
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, sessionInfo);
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, sessionInfo);
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, sessionInfo);
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, sessionInfo);
183
- return prompt;
184
-
185
- }
186
-
187
- }
188
-
189
- /**
190
- * Generic helper to check if message has a specific VXML action subType
191
- * @param {Object} msg - The message object
192
- * @param {string} subType - The subType to check for
193
- * @returns {boolean}
194
- */
195
- _hasSettingsSubType(msg, subType) {
196
- const commands = msg.attributes?.commands;
197
- if (!commands) return false;
198
- const settingsElement = commands.find((command) => command.type === SETTING_MESSAGE);
199
- return settingsElement?.subType === subType;
200
- }
201
-
202
- checkIfIsDTMFMenuMessage(msg) {
203
- return this._hasSettingsSubType(msg, TYPE_ACTION_VXML.DTMF_MENU);
204
- }
205
-
206
- checkIfIsDTMFForm(msg){
207
- return this._hasSettingsSubType(msg, TYPE_ACTION_VXML.DTMF_FORM);
208
- }
209
-
210
- checkIfIsBlindFransfer(msg){
211
- return this._hasSettingsSubType(msg, TYPE_ACTION_VXML.BLIND_TRANSFER);
212
- }
213
-
214
- checkIfIsSpeechFormMessage(msg){
215
- return this._hasSettingsSubType(msg, TYPE_ACTION_VXML.SPEECH_FORM);
216
- }
217
-
218
- checkIfIsWait(msg){
219
- const commands = msg.attributes?.commands;
220
- if (!commands) return false;
221
- const hasWait = commands.some((command) => command.type === WAIT_MESSAGE);
222
- const hasMessage = commands.some((command) => command.type === TEXT_MESSAGE);
223
- return hasWait && !hasMessage;
224
- }
225
-
226
- setVXMLAttributes(commands, attributes){
227
- const settingsCommand = commands.slice(-1)[0];
228
- if(settingsCommand?.settings){
229
- Object.assign(attributes, settingsCommand.settings);
230
- }
231
- return attributes;
232
- }
233
-
234
- toTiledesk(vxmlMessage) {
235
- winston.debug("[TiledeskVXMLTranslator] vxml message: ", vxmlMessage);
236
- }
237
-
238
- getMessageFromTdMessage(msg) {
239
- const commands = msg.attributes?.commands;
240
- if (!commands || commands.length === 0) return "";
241
-
242
- const command = commands[1];
243
- return command?.type === "message" ? command.message.text : "";
244
- }
245
-
246
- getButtonsFromCommand(command) {
247
- let buttons = [];
248
-
249
- if (command.type === "message" && command.message.attributes && command.message.attributes.attachment && command.message.attributes.attachment.buttons) {
250
- /* FILTER only 'action' button type */
251
- buttons = command.message.attributes.attachment.buttons.filter( (button) => (button.type === "action") );
252
- }
253
- return buttons;
254
- }
255
-
256
- getButtonsFromTdMessage(msg) {
257
- const commands = msg.attributes?.commands;
258
- if (!commands || commands.length === 0) return [];
259
-
260
- const command = commands[1];
261
- if (command?.type === "message" && command.message?.attributes?.attachment?.buttons) {
262
- return command.message.attributes.attachment.buttons.filter((button) => button.type === "action");
263
- }
264
- return [];
265
- }
266
-
267
- async hangupCall(rootEle){
268
- rootEle.ele("Hangup").up()
269
-
270
- return rootEle.end({ pretty: true });
271
- }
272
-
273
-
274
- async delayVXMLConverter(rootEle, message, xmlAttributes, sessionInfo){
275
- const command = message.attributes.commands[0]
276
-
277
- const prompt = this.promptVXML(rootEle, message, xmlAttributes, sessionInfo);
278
-
279
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/nextblock/' + xmlAttributes.callSid).up()
280
-
281
- return rootEle.end({ pretty: true });
282
- }
283
-
284
-
285
- async playPromptVXMLConverter(rootEle, message, xmlAttributes, sessionInfo){
286
-
287
- const prompt = await this.promptVXML(rootEle, message, xmlAttributes, sessionInfo);
288
-
289
- // const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + '&previousIntentTimestamp='+Date.now();
290
- // rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/nextblock/' + xmlAttributes.callSid + queryUrl).up()
291
- //prompt.ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl +'/nextblock/' + session.connection.calltoken", method: "post", namelist: "usertext session intentName previousIntentTimestamp" });
292
-
293
- return rootEle.end({ pretty: true });
294
-
295
- }
296
-
297
- /** DONE **/
298
- async speechFormVXMLConverter(rootEle, message, xmlAttributes, sessionInfo) {
299
-
300
- if(this.voiceProvider === VOICE_PROVIDER.TWILIO){
301
- const gather = rootEle.ele("Gather", { input: "speech"})
302
-
303
- const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
304
- gather.att("action", this.BASE_URL + '/nextBlock/' + xmlAttributes.callSid + queryUrl)
305
- // gather.att("action", this.BASE_URL + '/speechresult/' + xmlAttributes.callSid + queryUrl)
306
- .att("method", "POST")
307
- .att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
308
- .att('speechTimeout', "auto")
309
- .att("enhanced", "true") // enable enhanced recognition
310
-
311
- //if(xmlAttributes && xmlAttributes.noInputTimeout){
312
- // gather.att("timeout", Math.round(xmlAttributes.noInputTimeout/1000) ).up();
313
- //}
314
- if(xmlAttributes && xmlAttributes.incompleteSpeechTimeout){
315
- gather.att("speechTimeout", Math.round(xmlAttributes.incompleteSpeechTimeout/1000) ).up();
316
- }
317
-
318
- const prompt = this.promptVXML(gather, message, xmlAttributes, sessionInfo);
319
-
320
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
321
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
322
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
323
- }
324
-
325
- }else{
326
-
327
- const prompt = await this.promptVXML(rootEle, message, xmlAttributes, sessionInfo);
328
-
329
- const record = rootEle.ele("Record", { playBeep: "false"})
330
-
331
- let queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
332
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
333
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
334
- queryUrl += '&'+ handleNoInputNoMatchQuery.queryNoInput
335
- }
336
-
337
- record
338
- .att("action", this.BASE_URL + '/record/action/' + xmlAttributes.callSid + queryUrl)
339
- .att("method", "POST")
340
- .att("trim", "trim-silence")
341
- .att("timeout", "2")
342
- .att("recordingStatusCallback", this.BASE_URL + '/record/callback/' + xmlAttributes.callSid + queryUrl)
343
- .att("recordingStatusCallbackMethod", "POST")
344
-
345
- // if(xmlAttributes && xmlAttributes.noInputTimeout){
346
- // record.att("timeout", xmlAttributes.noInputTimeout/1000 ).up();
347
- // }
348
-
349
- }
350
-
351
- return rootEle.end({ pretty: true });
352
- }
353
-
354
- /** DONE **/
355
- async menuVXMLConverter(rootEle, message, xmlAttributes, sessionInfo) {
356
- const lastMessageCommand = message.attributes.commands.slice(-3)[0];
357
- const options = this.getButtonsFromCommand(lastMessageCommand);
358
-
359
- let menu_options = ''
360
- options.forEach((option) => menu_options += option.value + ':' + option.action.substring(1) + ';')
361
-
362
-
363
- let queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + '&previousIntentTimestamp='+Date.now() + '&menu_options=' + menu_options;
364
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
365
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoMatch){
366
- queryUrl += '&'+ handleNoInputNoMatchQuery.queryNoMatch
367
- }
368
-
369
- const gather = rootEle.ele("Gather", { input: "dtmf"})
370
-
371
-
372
- gather.att("timeout", Math.round(xmlAttributes.noInputTimeout/1000) )
373
- .att("numDigits", "1" )
374
- .att("action", this.BASE_URL + '/menublock/' + xmlAttributes.callSid + queryUrl)
375
- .att("method", "POST")
376
- .att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
377
-
378
- const prompt = await this.promptVXML(gather, message, xmlAttributes, sessionInfo);
379
-
380
-
381
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
382
- rootEle.ele("Redirect", {}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
383
- }
384
-
385
- return rootEle.end({ pretty: true });
386
- }
387
-
388
- /** DONE **/
389
- async dtmfFormVXMLConverter(rootEle, message, xmlAttributes, sessionInfo) {
390
-
391
- const gather = rootEle.ele("Gather", { input: "dtmf"})
392
-
393
- const queryUrl = '?intentName='+ querystring.encode(xmlAttributes.intentName) + "&previousIntentTimestamp="+Date.now();
394
- gather.att("timeout", Math.round(xmlAttributes.noInputTimeout/1000) )
395
- .att("action", this.BASE_URL + '/menublock/' + xmlAttributes.callSid + queryUrl)
396
- .att("method", "POST")
397
- .att("language", xmlAttributes.TTS_VOICE_LANGUAGE)
398
- const settings = await this.optionsVXML(gather, message, xmlAttributes);
399
-
400
- const prompt = await this.promptVXML(gather, message, xmlAttributes, sessionInfo);
401
-
402
- const handleNoInputNoMatchQuery = await this.handleNoInputNoMatch(rootEle, message, xmlAttributes);
403
- if(handleNoInputNoMatchQuery && handleNoInputNoMatchQuery.queryNoInput){
404
- rootEle.ele("Redirect", {method: "POST"}, this.BASE_URL + '/handle/' + xmlAttributes.callSid + '/no_input?'+ handleNoInputNoMatchQuery.queryNoInput)
405
- }
406
-
407
- //.ele('disconnect')
408
- return rootEle.end({ pretty: true });
409
- }
410
-
411
-
412
- async blindTransferVXMLConverter(rootEle, message, xmlAttributes, sessionInfo) {
413
- const lastMessageCommand = message.attributes.commands.slice(-3)[0];
414
- const options = this.getButtonsFromCommand(lastMessageCommand);
415
-
416
- const prompt = await this.promptVXML(rootEle, message, xmlAttributes, sessionInfo);
417
- const transfer = await this.transferVXML(rootEle, message, xmlAttributes)
418
-
419
-
420
- return rootEle.end({ pretty: true });
421
- }
422
-
423
- async headerVXML(rootEle, attributes) {
424
- //rootEle.att("xmlns", "http://www.w3.org/2001/vxml");
425
- //rootEle.att("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
426
- //rootEle.att( "xsi:schemaLocation", "http://www.w3.org/2001/vxml http://www.w3.org/TR/2007/REC-voicexml21-20070619/vxml.xsd");
427
- //rootEle.att("version", "2.1");
428
- //rootEle.att("xml:lang", attributes.TTS_VOICE_LANGUAGE);
429
-
430
- rootEle.ele("Parameter", { name: "callSid", value: "'" + attributes.callSid + "'" }).up();
431
- rootEle.ele("Parameter", { name: "intentName", value: "'" + attributes.intentName + "'"}).up();
432
- rootEle.ele("Parameter", { name: "previousIntentTimestamp", value: "'" + Date.now() + "'"}).up();
433
- rootEle.ele("Parameter", { name: "proxyBaseUrl", value: "'" + this.BASE_URL + "'"}).up();
434
- const catchVXML = this.catchVXMLEvent(rootEle, attributes);
435
- return rootEle;
436
- }
437
-
438
- async catchVXMLEvent(rootEle, attributes) {
439
- //rootEle.ele("var", { name: "disconnection_url", expr: "'https://tiledesk-vxml-connector.glitch.me/event/'"}).up()
440
- rootEle.ele("catch", { event: "connection.disconnect" })
441
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl+'/event/' + session.connection.calltoken + '/disconnect'", method: "post", namelist: "intentName previousIntentTimestamp"}).up();
442
- rootEle.ele("catch", { event: "connection.disconnect.hangup" })
443
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl+'/event/' + session.connection.calltoken + '/hangup'", method: "post", namelist: "intentName previousIntentTimestamp"}).up();
444
- rootEle.ele("catch", { event: "error.noresource.asr" })
445
- .ele("submit", { fetchhint: "safe", expr: "proxyBaseUrl+'/event/' + session.connection.calltoken + '/no-resources?event=' + application._event", method: "post", namelist: "intentName previousIntentTimestamp" }).up();
446
- return rootEle;
447
- }
448
-
449
-
450
- async handleNoInputNoMatch(rootEle, msg, attributes){
451
- const dtmf_form_element = msg.attributes?.commands?.find((command) => command.type === SETTING_MESSAGE);
452
- if (!dtmf_form_element?.settings) {
453
- return null;
454
- }
455
-
456
- const { noInputIntent, noMatchIntent } = dtmf_form_element.settings;
457
-
458
- // Build query strings for noInput and noMatch intents
459
- const queryNoInput = noInputIntent ? `button_action=${noInputIntent.substring(1)}` : '';
460
- const queryNoMatch = noMatchIntent ? `button_action=${noMatchIntent.substring(1)}` : '';
461
-
462
- return { queryNoInput, queryNoMatch };
463
- }
464
-
465
- /* create PROMPT section */
466
- async promptVXML(rootEle, msg, attributes, sessionInfo) {
467
- //let promt = rootEle.ele("prompt", {bargein: attributes.bargein});
468
-
469
- if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
470
- let commands = msg.attributes.commands;
471
-
472
- for (const command of commands) {
473
- if (command.type === "message") {
474
- //case type: TEXT
475
- if(command.message.type === 'text'){
476
- let text = utils.markdownToTwilioSpeech(command.message.text);
477
- if(this.voiceProvider !== VOICE_PROVIDER.TWILIO){
478
- let voiceMessageUrl = await this.generateTTS(text, attributes, sessionInfo);
479
- rootEle.ele('Play', { loop: 0}, voiceMessageUrl );
480
- this.lastCallSidVerb[attributes.callSid] = "play";
481
- }else{
482
- rootEle.ele("Say", { voice: attributes.TTS_VOICE_NAME, language: attributes.TTS_VOICE_LANGUAGE }, text);
483
- this.lastCallSidVerb[attributes.callSid] = "say";
484
- }
485
-
486
- }
487
- //case type: FRAME
488
- if(command.message.type === 'frame' && command.message.metadata.src !== ""){
489
- if(command.message.text != ''){
490
- let text = utils.markdownToTwilioSpeech(command.message.text);
491
- rootEle.ele("Say", { voice: attributes.TTS_VOICE_NAME, language: attributes.TTS_VOICE_LANGUAGE }, text);
492
- }
493
- rootEle.ele('Play', { loop: 0 }, command.message.metadata.src )
494
- this.lastCallSidVerb[attributes.callSid] = "play";
495
-
496
- }
497
- } else if (command.type === "wait" && command.time !== 0) {
498
- rootEle.ele("Pause", { length: command.time/1000 }).up();
499
- this.lastCallSidVerb[attributes.callSid] = "pause";
500
- }
501
- }
502
- }
503
- return rootEle;
504
- }
505
-
506
- async optionsVXML(rootEle, msg, attributes){
507
-
508
- if (msg.attributes && msg.attributes.commands && msg.attributes.commands.length > 0 ) {
509
- let commands = msg.attributes.commands;
510
- let dtmf_element = commands.find((command) => command.type === SETTING_MESSAGE)
511
-
512
- if(dtmf_element.settings.minDigits){
513
- attributes.maxDigits = dtmf_element.settings.minDigits
514
- }
515
- if(dtmf_element.settings.maxDigits){
516
- attributes.maxDigits = dtmf_element.settings.maxDigits
517
- rootEle.att('numDigits', attributes.maxDigits)
518
- }
519
- if(dtmf_element.settings.terminators){
520
- attributes.terminators = dtmf_element.settings.terminators
521
- rootEle.att('finishOnKey', attributes.terminators)
522
- }
523
- }
524
- return rootEle
525
- }
526
-
527
- async transferVXML(rootEle, msg, attributes){
528
- const lastCommand = msg.attributes.commands.slice(-1)[0];
529
-
530
- const transfer = rootEle.ele("Dial");
531
-
532
- let queryUrl = '?intentName='+ querystring.encode(attributes.intentName) + '&previousIntentTimestamp='+Date.now()
533
- /* <-- transfer error --> */
534
- if(lastCommand.settings && lastCommand.settings.falseIntent){
535
- queryUrl += '&' + 'button_success=' + attributes.trueIntent.substring(1)
536
-
537
- }
538
-
539
- /* <!-- trasfer OK --> */
540
- if(lastCommand.settings && lastCommand.settings.trueIntent){
541
- queryUrl += '&' + 'button_failure=' + attributes.falseIntent.substring(1)
542
- }
543
-
544
- if(lastCommand.settings && lastCommand.settings.transferTo){
545
- const regexOnlyNumber = new RegExp(/^.*\d.*$/gm)
546
- if(lastCommand.settings.transferTo.match(regexOnlyNumber)){
547
- const number = transfer.ele('Number', {}, lastCommand.settings.transferTo)
548
- number.att('statusCallbackEvent', 'initiated ringing answered completed')
549
- .att('statusCallback', this.BASE_URL + '/event/' + attributes.callSid + '/transfer?' + queryUrl )
550
- .att('statusCallbackMethod', 'POST')
551
- }else{
552
- transfer.att('Sip', {},"'sip:"+ lastCommand.settings.transferTo + attributes.uriTransferParameters+"'")
553
- }
554
-
555
- }
556
-
557
- return transfer.up()
558
- }
559
-
560
- async generateTTS(text, attributes, sessionInfo){
561
- let audioData = null;
562
- try {
563
- switch(this.voiceProvider){
564
- case VOICE_PROVIDER.OPENAI:
565
- let GPT_KEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
566
- audioData = await this.aiService.textToSpeech(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, GPT_KEY)
567
- break;
568
- case VOICE_PROVIDER.ELEVENLABS:
569
- let ELEVENLABS_APIKEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
570
- audioData = await this.aiService.textToSpeechElevenLabs(text, attributes.TTS_VOICE_NAME, attributes.TTS_MODEL, attributes.TTS_VOICE_LANGUAGE, ELEVENLABS_APIKEY)
571
- break;
572
- default:
573
- throw new SttError('TTS_FAILED', 'Unsupported voice provider: ' + this.voiceProvider);
574
- }
575
-
576
- if (!audioData) {
577
- throw new SttError('TTS_FAILED', 'TTS returned no audio data');
578
- }
579
-
580
- let fileUrl = await this.uploadService.upload(attributes.callSid, audioData, sessionInfo.user)
581
- winston.debug('(voice) Audio Message url captured after TTS -->', fileUrl)
582
- return fileUrl
583
- } catch (error) {
584
- winston.error('(voice) TTS generation error:', error);
585
- switch (error.code) {
586
- case 'TTS_FAILED':
587
- winston.error('(voice) TTS_FAILED:', error.message);
588
- break;
589
- case 'AI_SERVICE_ERROR':
590
- winston.error('(voice) AI_SERVICE_ERROR:', error.message);
591
- break;
592
- case 'UPLOAD_SERVICE_ERROR':
593
- winston.error('(voice) UPLOAD_SERVICE_ERROR:', error.message);
594
- break;
595
- default:
596
- throw new SttError('TTS_FAILED', 'TTS generation failed: ' + error.message);
597
- }
598
- }
599
-
600
- }
601
-
602
-
603
- async jsonToVxmlConverter(json) {
604
- const root = xmlbuilder.create("vxml");
605
- root.ele("prompt", {}, json.prompt);
606
-
607
- const form = root.ele("form");
608
- json.options.forEach((option) => form.ele("option", {}, option));
609
-
610
- return root.end({ pretty: true });
611
- }
612
- }
613
-
614
- module.exports = { TiledeskTwilioTranslator };