@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.
- package/LICENSE +179 -0
- package/README.md +515 -0
- package/index.js +7 -1562
- package/package.json +23 -21
- package/src/app.js +154 -0
- package/src/config/index.js +32 -0
- package/src/controllers/VoiceController.js +493 -0
- package/src/middlewares/httpLogger.js +43 -0
- package/src/models/KeyValueStore.js +78 -0
- package/src/routes/manageApp.js +298 -0
- package/src/routes/voice.js +22 -0
- package/src/services/AiService.js +219 -0
- package/src/services/AiService.sdk.js +367 -0
- package/src/services/IntegrationService.js +74 -0
- package/src/services/MessageService.js +139 -0
- package/src/services/README_SDK.md +107 -0
- package/src/services/SessionService.js +143 -0
- package/src/services/SpeechService.js +134 -0
- package/src/services/TiledeskMessageBuilder.js +135 -0
- package/src/services/TwilioService.js +129 -0
- package/src/services/UploadService.js +78 -0
- package/src/services/channels/TiledeskChannel.js +268 -0
- package/{tiledesk → src/services/channels}/VoiceChannel.js +20 -59
- package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
- package/src/services/index.js +45 -0
- package/src/services/translators/TiledeskTwilioTranslator.js +514 -0
- package/src/utils/fileUtils.js +24 -0
- package/src/utils/logger.js +32 -0
- package/{tiledesk → src/utils}/utils-message.js +6 -21
- package/logs/app.log +0 -3082
- package/routes/manageApp.js +0 -419
- package/tiledesk/KVBaseMongo.js +0 -101
- package/tiledesk/TiledeskChannel.js +0 -363
- package/tiledesk/TiledeskSubscriptionClient.js +0 -135
- package/tiledesk/TiledeskTwilioTranslator.js +0 -707
- package/tiledesk/fileUtils.js +0 -55
- package/tiledesk/services/AiService.js +0 -230
- package/tiledesk/services/IntegrationService.js +0 -81
- package/tiledesk/services/UploadService.js +0 -88
- /package/{winston.js → src/config/logger.js} +0 -0
- /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
- /package/{template → src/template}/configure.html +0 -0
- /package/{template → src/template}/css/configure.css +0 -0
- /package/{template → src/template}/css/error.css +0 -0
- /package/{template → src/template}/css/style.css +0 -0
- /package/{template → src/template}/error.html +0 -0
- /package/{tiledesk → src/utils}/constants.js +0 -0
- /package/{tiledesk → src/utils}/errors.js +0 -0
- /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 };
|