@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.
- package/README.md +494 -23
- package/package.json +3 -2
- package/src/app.js +14 -5
- package/src/controllers/VoiceController.js +33 -28
- package/src/middlewares/httpLogger.js +25 -13
- package/src/models/KeyValueStore.js +5 -5
- package/src/routes/manageApp.js +7 -7
- package/src/services/AiService.js +8 -8
- package/src/services/AiService.sdk.js +13 -13
- package/src/services/IntegrationService.js +2 -2
- package/src/services/MessageService.js +12 -6
- package/src/services/SessionService.js +5 -5
- package/src/services/SpeechService.js +3 -3
- package/src/services/TwilioService.js +9 -2
- package/src/services/UploadService.js +1 -1
- package/src/services/channels/TiledeskChannel.js +54 -55
- package/src/services/channels/VoiceChannel.js +4 -4
- package/src/services/clients/TiledeskSubscriptionClient.js +2 -2
- package/src/services/translators/TiledeskTwilioTranslator.js +20 -15
- package/src/controllers/VoiceController.original.js +0 -811
- package/src/services/translators/TiledeskTwilioTranslator.original.js +0 -614
|
@@ -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 };
|