@tiledesk/tiledesk-voice-twilio-connector 0.1.28 → 0.2.0-rc3
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 +44 -0
- package/index.js +7 -1562
- package/package.json +23 -22
- package/src/app.js +146 -0
- package/src/config/index.js +32 -0
- package/src/controllers/VoiceController.js +488 -0
- package/src/controllers/VoiceController.original.js +811 -0
- package/src/middlewares/httpLogger.js +31 -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 +133 -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 +122 -0
- package/src/services/UploadService.js +78 -0
- package/src/services/channels/TiledeskChannel.js +269 -0
- package/{tiledesk → src/services/channels}/VoiceChannel.js +17 -56
- package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
- package/src/services/index.js +45 -0
- package/src/services/translators/TiledeskTwilioTranslator.js +509 -0
- package/{tiledesk/TiledeskTwilioTranslator.js → src/services/translators/TiledeskTwilioTranslator.original.js} +119 -212
- 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/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
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
const { TiledeskChannel } = require('../services/channels/TiledeskChannel');
|
|
2
|
+
const { TiledeskTwilioTranslator } = require('../services/translators/TiledeskTwilioTranslator');
|
|
3
|
+
const utilsMessage = require('../utils/utils-message');
|
|
4
|
+
const utils = require('../utils/utils');
|
|
5
|
+
const { TYPE_MESSAGE, CHANNEL_NAME, VOICE_LANGUAGE, VOICE_NAME, CALL_STATUS, VOICE_PROVIDER } = require('../utils/constants');
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
const querystring = require('querystring');
|
|
8
|
+
|
|
9
|
+
class VoiceController {
|
|
10
|
+
constructor(services) {
|
|
11
|
+
this.aiService = services.aiService;
|
|
12
|
+
this.uploadService = services.uploadService;
|
|
13
|
+
this.integrationService = services.integrationService;
|
|
14
|
+
this.voiceChannel = services.voiceChannel;
|
|
15
|
+
this.db = services.db;
|
|
16
|
+
this.redisClient = services.redisClient;
|
|
17
|
+
this.config = services.config;
|
|
18
|
+
|
|
19
|
+
this.tdChannel = new TiledeskChannel({
|
|
20
|
+
API_URL: this.config.API_URL,
|
|
21
|
+
redis_client: this.redisClient
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.tdTranslator = new TiledeskTwilioTranslator({
|
|
25
|
+
BASE_URL: this.config.BASE_URL,
|
|
26
|
+
aiService: this.aiService,
|
|
27
|
+
uploadService: this.uploadService
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async index(req, res) {
|
|
32
|
+
res.send("Tiledesk Voice Connector");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async tiledesk(req, res) {
|
|
36
|
+
try {
|
|
37
|
+
logger.debug("(voice) Message received from Tiledesk in projectID: " + req.body.payload.id_project + ' ---- and text: ' + req.body.payload.text);
|
|
38
|
+
let tiledeskMessage = req.body.payload;
|
|
39
|
+
let project_id = tiledeskMessage.id_project;
|
|
40
|
+
|
|
41
|
+
/*SKIP INFO MESSAGES*/
|
|
42
|
+
/*SKIP CURRENT USER MESSAGES*/
|
|
43
|
+
if (!utilsMessage.messageType(TYPE_MESSAGE.INFO, tiledeskMessage) && !(tiledeskMessage.sender.indexOf("vxml") > -1)) {
|
|
44
|
+
logger.debug(`> whook SAVE MESSAGE "${tiledeskMessage.text}" TO QUEUE at time ` + new Date());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (await this.tdChannel.addMessageToQueue(tiledeskMessage)) {
|
|
48
|
+
if (tiledeskMessage.attributes && tiledeskMessage.attributes.flowAttributes && tiledeskMessage.attributes.flowAttributes.CallSid && this.tdTranslator.lastCallSidVerb[tiledeskMessage.attributes.flowAttributes.CallSid] == 'play') {
|
|
49
|
+
|
|
50
|
+
const CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
51
|
+
let settings = await this.db.get(CONTENT_KEY);
|
|
52
|
+
|
|
53
|
+
const twilio = require('twilio')(settings.account_sid, settings.auth_token);
|
|
54
|
+
const callSid = tiledeskMessage.attributes.flowAttributes.CallSid;
|
|
55
|
+
const queryString = '?intentName=' + querystring.encode(tiledeskMessage.attributes.intentName) + '&previousIntentTimestamp=' + Date.now();
|
|
56
|
+
try {
|
|
57
|
+
await twilio.calls(callSid).update({
|
|
58
|
+
url: `${this.config.BASE_URL}/nextblock/${callSid}${queryString}`,
|
|
59
|
+
method: 'POST'
|
|
60
|
+
});
|
|
61
|
+
logger.debug(`(voice) Call ${callSid} redirected to /nextblock`);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
logger.error('(voice) Error redirecting call to /nextblock:', e)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
res.send("(voice) Message received from Voice Twilio Proxy");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error("(voice) Error in tiledesk handler:", error);
|
|
72
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async webhook(req, res) {
|
|
77
|
+
try {
|
|
78
|
+
let start_call = new Date().getTime();
|
|
79
|
+
logger.debug('(voice) called POST /webhook/:id_project ' + new Date(), req.params);
|
|
80
|
+
|
|
81
|
+
let project_id = req.params.id_project;
|
|
82
|
+
let callSid = req.body.CallSid;
|
|
83
|
+
let from = req.body.From;
|
|
84
|
+
let to = req.body.To;
|
|
85
|
+
|
|
86
|
+
if ((!from || !to) && from !== "client:Anonymous") {
|
|
87
|
+
return res.status(404).send({ error: "Error: Missing from/to parameters" });
|
|
88
|
+
}
|
|
89
|
+
from = utils.getNumber(from); //remove '+' from number
|
|
90
|
+
if (to) {
|
|
91
|
+
to = utils.getNumber(to); //remove '+' from number
|
|
92
|
+
} else {
|
|
93
|
+
to = "client:AnonymousReceiver";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
97
|
+
let settings = await this.db.get(CONTENT_KEY);
|
|
98
|
+
if (!settings) {
|
|
99
|
+
return res.status(404).send({ error: "VOICE Channel not already connected" });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let vxmlAttributes = {
|
|
103
|
+
TTS_VOICE_LANGUAGE: VOICE_LANGUAGE,
|
|
104
|
+
TTS_VOICE_NAME: VOICE_NAME,
|
|
105
|
+
callSid: callSid
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let start2 = new Date().getTime();
|
|
109
|
+
let user = await this.tdChannel.signIn(from, settings);
|
|
110
|
+
if (!user) {
|
|
111
|
+
res.status(401).send({ message: "Cannot able to signIn with current caller phone :" + from });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let end2 = new Date().getTime();
|
|
115
|
+
|
|
116
|
+
// Parallelize conversation generation and integration key retrieval
|
|
117
|
+
let [conversation_id, openaiKeyResult, elevenLabsKey] = await Promise.all([
|
|
118
|
+
this.tdChannel.generateConversation(from, callSid, project_id),
|
|
119
|
+
|
|
120
|
+
// Retrieve OpenAI key (with sequential fallback)
|
|
121
|
+
(async () => {
|
|
122
|
+
try {
|
|
123
|
+
let key = await this.integrationService.getKeyFromIntegrations(project_id, 'openai', settings.token)
|
|
124
|
+
if (!key) {
|
|
125
|
+
logger.debug("(voice) - Key not found in Integrations. Searching in kb settings...");
|
|
126
|
+
key = await this.integrationService.getKeyFromKbSettings(project_id, settings.token);
|
|
127
|
+
}
|
|
128
|
+
if (!key) {
|
|
129
|
+
logger.debug("(voice) - Retrieve public gptkey")
|
|
130
|
+
key = this.config.GPT_KEY;
|
|
131
|
+
return { key, publicKey: true };
|
|
132
|
+
}
|
|
133
|
+
return { key, publicKey: false };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.error('(voice) - Error retrieving OpenAI key:', error);
|
|
136
|
+
return { key: this.config.GPT_KEY, publicKey: true };
|
|
137
|
+
}
|
|
138
|
+
})(),
|
|
139
|
+
|
|
140
|
+
// Retrieve ElevenLabs key in parallel
|
|
141
|
+
this.integrationService.getKeyFromIntegrations(project_id, 'elevenlabs', settings.token).catch((error) => {
|
|
142
|
+
logger.error('(voice) - Error retrieving ElevenLabs key:', error);
|
|
143
|
+
return null;
|
|
144
|
+
})
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
logger.debug("(voice) conversation returned:" + conversation_id);
|
|
148
|
+
|
|
149
|
+
// Build integrations array
|
|
150
|
+
let integrations = [];
|
|
151
|
+
if (openaiKeyResult && openaiKeyResult.key) {
|
|
152
|
+
integrations.push({ type: 'openai', key: openaiKeyResult.key, publicKey: openaiKeyResult.publicKey })
|
|
153
|
+
}
|
|
154
|
+
if (elevenLabsKey) {
|
|
155
|
+
logger.debug("(voice) - Key found in Integrations: " + elevenLabsKey);
|
|
156
|
+
integrations.push({ type: 'elevenlabs', key: elevenLabsKey, publicKey: false })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//save data to redis
|
|
160
|
+
let session_data = {
|
|
161
|
+
from: from,
|
|
162
|
+
to: to,
|
|
163
|
+
callSid: callSid,
|
|
164
|
+
project_id: project_id,
|
|
165
|
+
user: user,
|
|
166
|
+
conversation_id: conversation_id,
|
|
167
|
+
integrations: integrations
|
|
168
|
+
}
|
|
169
|
+
this.voiceChannel.setSessionForCallId(callSid, session_data)
|
|
170
|
+
|
|
171
|
+
let tiledeskMessage = {
|
|
172
|
+
text: '/start',
|
|
173
|
+
senderFullname: from,
|
|
174
|
+
type: 'text',
|
|
175
|
+
attributes: {
|
|
176
|
+
subtype: 'info',
|
|
177
|
+
payload: {
|
|
178
|
+
...req.body //send all attributes back to chatbot
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
channel: { name: CHANNEL_NAME },
|
|
182
|
+
departmentid: settings.department_id
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
let response = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id)
|
|
186
|
+
if (!response) {
|
|
187
|
+
return res.status(503).send({ message: "Bad response: Quota exceeded" })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//await for a response message from tiledesk queue
|
|
191
|
+
let start_time_get_message = new Date()
|
|
192
|
+
let message = await this._getMessage(callSid, from, project_id, conversation_id, this.tdChannel)
|
|
193
|
+
let end_time_get_message = new Date()
|
|
194
|
+
logger.verbose(`Time to getMessage from queue in /webhook/:${project_id} : ${(end_time_get_message - start_time_get_message)}[ms] --- at time:` + new Date())
|
|
195
|
+
|
|
196
|
+
// send standard wait vxml message
|
|
197
|
+
let messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, session_data)
|
|
198
|
+
logger.debug('(voice) /webhook/:id_project messageVXML-->' + messageToVXML)
|
|
199
|
+
|
|
200
|
+
let end_call1 = new Date().getTime();
|
|
201
|
+
logger.info(`Time to respond to /webhook/${project_id} before response: ${(end_call1 - start_call)}[ms]`)
|
|
202
|
+
|
|
203
|
+
// Render the response as XML in reply to the webhook request
|
|
204
|
+
res.set('Content-Type', 'text/xml');
|
|
205
|
+
res.status(200).send(messageToVXML);
|
|
206
|
+
|
|
207
|
+
let end_call2 = new Date().getTime();
|
|
208
|
+
logger.info(`Time to respond to /webhook/${project_id}: ${(end_call2 - start_call)}[ms]`)
|
|
209
|
+
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error("(voice) Error in webhook handler:", error);
|
|
212
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async _initializeContext(callSid) {
|
|
217
|
+
const sessionInfo = await this.voiceChannel.getSessionForCallId(callSid);
|
|
218
|
+
if (!sessionInfo) {
|
|
219
|
+
throw new Error("Can't retrieve data for callSid -> " + callSid);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { project_id, from, conversation_id, user } = sessionInfo;
|
|
223
|
+
|
|
224
|
+
const vxmlAttributes = {
|
|
225
|
+
TTS_VOICE_LANGUAGE: VOICE_LANGUAGE,
|
|
226
|
+
TTS_VOICE_NAME: VOICE_NAME,
|
|
227
|
+
callSid: callSid,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return { sessionInfo, project_id, from, conversation_id, user, vxmlAttributes };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Process user speech/text input and return VXML response.
|
|
235
|
+
* Shared logic for nextblock and speechresult handlers.
|
|
236
|
+
* @param {string} usertext - The speech result text
|
|
237
|
+
* @param {string} callSid - The call session ID
|
|
238
|
+
* @param {string} endpoint - Endpoint name for logging
|
|
239
|
+
* @returns {Promise<{message: Object, vxmlAttributes: Object, sessionInfo: Object}>}
|
|
240
|
+
*/
|
|
241
|
+
async _processUserSpeech(usertext, callSid, endpoint) {
|
|
242
|
+
const { sessionInfo, project_id, from, conversation_id, user, vxmlAttributes } = await this._initializeContext(callSid);
|
|
243
|
+
|
|
244
|
+
let message;
|
|
245
|
+
logger.debug(`(voice) ******* user text --> ${usertext}`);
|
|
246
|
+
|
|
247
|
+
if (!usertext) {
|
|
248
|
+
const start = Date.now();
|
|
249
|
+
message = await this._getMessage(callSid, from, project_id, conversation_id, this.tdChannel);
|
|
250
|
+
logger.verbose(`(if) Time to getMessage from queue in /${endpoint}/${callSid} : ${Date.now() - start}[ms]`);
|
|
251
|
+
} else {
|
|
252
|
+
const tiledeskMessage = {
|
|
253
|
+
text: usertext,
|
|
254
|
+
senderFullname: from,
|
|
255
|
+
type: 'text',
|
|
256
|
+
channel: { name: CHANNEL_NAME }
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const sendStart = Date.now();
|
|
260
|
+
const tdMessage = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
261
|
+
logger.verbose(`(else) Time to send message to tiledesk in /${endpoint}/${callSid} : ${Date.now() - sendStart}[ms] with text ${tdMessage?.text}`);
|
|
262
|
+
|
|
263
|
+
const getStart = Date.now();
|
|
264
|
+
message = await this._getMessage(callSid, from, project_id, conversation_id, this.tdChannel);
|
|
265
|
+
logger.verbose(`(else) Time to getMessage from queue in /${endpoint}/${callSid} : ${Date.now() - getStart}[ms]`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { message, vxmlAttributes, sessionInfo, callSid };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async nextblock(req, res) {
|
|
272
|
+
try {
|
|
273
|
+
const start_call = Date.now();
|
|
274
|
+
logger.verbose(`(voice) called POST /nextblock at ${new Date()} with text: ${req.body.SpeechResult}`);
|
|
275
|
+
|
|
276
|
+
const { message, vxmlAttributes, sessionInfo, callSid } = await this._processUserSpeech(
|
|
277
|
+
req.body.SpeechResult,
|
|
278
|
+
req.params.callSid,
|
|
279
|
+
'nextblock'
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
283
|
+
logger.debug("(voice) VXML to SEND: " + messageToVXML);
|
|
284
|
+
logger.info(`Time to respond to /nextblock/${callSid} : ${Date.now() - start_call}[ms]`);
|
|
285
|
+
|
|
286
|
+
res.set('Content-Type', 'application/xml');
|
|
287
|
+
res.status(200).send(messageToVXML);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
logger.error("(voice) Error in nextblock handler:", error);
|
|
290
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async speechresult(req, res) {
|
|
295
|
+
try {
|
|
296
|
+
const start_call = Date.now();
|
|
297
|
+
logger.verbose(`(voice) called POST /speechresult at ${new Date()} with text: ${req.body.SpeechResult}`);
|
|
298
|
+
|
|
299
|
+
const { message, vxmlAttributes, sessionInfo, callSid } = await this._processUserSpeech(
|
|
300
|
+
req.body.SpeechResult,
|
|
301
|
+
req.params.callSid,
|
|
302
|
+
'speechresult'
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
306
|
+
logger.debug("(voice) VXML to SEND: " + messageToVXML);
|
|
307
|
+
logger.info(`Time to respond to /speechresult/${callSid} : ${Date.now() - start_call}[ms]`);
|
|
308
|
+
|
|
309
|
+
res.set('Content-Type', 'application/xml');
|
|
310
|
+
res.status(200).send(messageToVXML);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error("(voice) Error in speechresult handler:", error);
|
|
313
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async recordAction(req, res) {
|
|
318
|
+
try {
|
|
319
|
+
logger.verbose('+++++++++++(voice) called POST record/action/:callSid at time ' + new Date() + "at timestamp " + new Date().getTime());
|
|
320
|
+
let start_call = new Date();
|
|
321
|
+
|
|
322
|
+
let callSid = req.body.CallSid;
|
|
323
|
+
|
|
324
|
+
const { sessionInfo, project_id, from, conversation_id, vxmlAttributes } = await this._initializeContext(callSid);
|
|
325
|
+
|
|
326
|
+
let start_time_get_message = new Date()
|
|
327
|
+
let message = await this._getMessage(callSid, from, project_id, conversation_id, this.tdChannel)
|
|
328
|
+
logger.debug('message from getMessage in /record/action/: ', message)
|
|
329
|
+
let end_time_get_message = new Date()
|
|
330
|
+
logger.verbose(`Time to getMessage from queue in /record/action/${callSid} : ${(end_time_get_message - start_time_get_message)}[ms]` + ' --- at time:' + new Date())
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
// convert response to vxml
|
|
334
|
+
let messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
335
|
+
logger.debug("(voice) /record/action VXML to SEND: " + messageToVXML);
|
|
336
|
+
|
|
337
|
+
let end_call = new Date();
|
|
338
|
+
logger.info(`Time to respond to /record/action/${callSid} : ${(end_call - start_call)}[ms]`)
|
|
339
|
+
res.set('Content-Type', 'application/xml');
|
|
340
|
+
res.status(200).send(messageToVXML);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
logger.error("(voice) Error in recordAction handler:", error);
|
|
343
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async recordCallback(req, res) {
|
|
348
|
+
try {
|
|
349
|
+
logger.verbose('+++++++++++(voice) called POST record/callback/:callSid at time' + new Date() + "at timestamp " + new Date().getTime());
|
|
350
|
+
let start_call = new Date();
|
|
351
|
+
|
|
352
|
+
let callSid = req.params.callSid || req.body.CallSid;
|
|
353
|
+
let audioFileUrl = req.body.RecordingUrl;
|
|
354
|
+
let button_action = req.query.button_action ? '#' + req.query.button_action : '';
|
|
355
|
+
let previousIntentName = req.query.intentName || '';
|
|
356
|
+
|
|
357
|
+
const { sessionInfo, project_id, from, conversation_id, user } = await this._initializeContext(callSid);
|
|
358
|
+
|
|
359
|
+
const CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
360
|
+
let settings = await this.db.get(CONTENT_KEY);
|
|
361
|
+
if (!settings) {
|
|
362
|
+
return res.status(404).send({ error: "VOICE Channel not already connected" })
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let tiledeskMessage = null;
|
|
366
|
+
|
|
367
|
+
//SPEECH TO TEXT
|
|
368
|
+
const attributes = await this.voiceChannel.getSettingsForCallId(callSid);
|
|
369
|
+
logger.debug(`[VOICE] getting text message from STT: ${audioFileUrl}, model: ${attributes.STT_MODEL}`);
|
|
370
|
+
// generateSTT ritorna sempre un oggetto coerente (anche vuoto o /close)
|
|
371
|
+
tiledeskMessage = await this._generateSTT(audioFileUrl, attributes, sessionInfo, settings)
|
|
372
|
+
logger.debug('[VOICE] tiledeskMessage from STT: ', tiledeskMessage)
|
|
373
|
+
if (!tiledeskMessage || Object.keys(tiledeskMessage).length === 0) {
|
|
374
|
+
logger.debug(`[VOICE] STT result empty, fallback to no_input branch for callSid ${callSid}`);
|
|
375
|
+
tiledeskMessage = this._buildNoInputMessage('no_input', { from, button_action, payload: { event: 'no_input', lastBlock: previousIntentName, lastTimestamp: Date.now() } });
|
|
376
|
+
} else {
|
|
377
|
+
const normalizedText = utils.normalizeSTT(tiledeskMessage.text);
|
|
378
|
+
logger.verbose(`[VOICE] normalized STT text: ${normalizedText} for callSid ${callSid}`);
|
|
379
|
+
if (!normalizedText) {
|
|
380
|
+
tiledeskMessage = this._buildNoInputMessage('no_input', { from, button_action, payload: { event: 'no_input', lastBlock: previousIntentName, lastTimestamp: Date.now() } });
|
|
381
|
+
} else {
|
|
382
|
+
tiledeskMessage.text = normalizedText;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
//send message to tiledesk
|
|
387
|
+
let tdMessage = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
388
|
+
let end_call = new Date();
|
|
389
|
+
logger.info(`Time to respond to /record/callback/${callSid} : ${(end_call - start_call)} [ms] with text ` + tiledeskMessage.text);
|
|
390
|
+
|
|
391
|
+
res.status(200).send({ success: true, message: "Message sent to Tiledesk for callSid " + callSid });
|
|
392
|
+
} catch (error) {
|
|
393
|
+
logger.error("(voice) Error in recordCallback handler:", error);
|
|
394
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async menublock(req, res) {
|
|
399
|
+
try {
|
|
400
|
+
let start_call = new Date().getTime();
|
|
401
|
+
logger.debug("(voice) called POST /menu", req.body);
|
|
402
|
+
logger.debug("(voice) called POST /menu query", req.query);
|
|
403
|
+
logger.verbose('/menublock at: ' + new Date() + 'with text:' + req.body.Digits)
|
|
404
|
+
|
|
405
|
+
let message_text = '';
|
|
406
|
+
let attributes = {};
|
|
407
|
+
let button = {}
|
|
408
|
+
|
|
409
|
+
let callSid = req.params.callSid;
|
|
410
|
+
let buttons_menu = req.query.menu_options;
|
|
411
|
+
let buttonNoMatch = req.query.button_action;
|
|
412
|
+
|
|
413
|
+
let menu_choice = req.body.Digits || '';
|
|
414
|
+
|
|
415
|
+
/** use case: DTMF MENU **/
|
|
416
|
+
if (buttons_menu) {
|
|
417
|
+
buttons_menu.split(';').some((option) => {
|
|
418
|
+
option = option.split(':')
|
|
419
|
+
if (option[0] === menu_choice) {
|
|
420
|
+
button.value = option[0]
|
|
421
|
+
button.action = '#' + option[1]
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
/* case noMatch input: Digits is not in a valid menu option*/
|
|
428
|
+
if (Object.keys(button).length === 0) {
|
|
429
|
+
button.value = menu_choice
|
|
430
|
+
button.action = '#' + buttonNoMatch
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
message_text = button.value.toString();
|
|
434
|
+
attributes = {
|
|
435
|
+
action: button.action
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
} else {
|
|
439
|
+
/** use case: DTMF Speech **/
|
|
440
|
+
message_text = menu_choice.toString(); //.replace(/(\d)/g, '$1 '); //convert number to string and then add a space after each number
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
logger.debug("(voice) button menu: ", button);
|
|
444
|
+
logger.debug("(voice) message_text menu: " + message_text);
|
|
445
|
+
|
|
446
|
+
const { sessionInfo, project_id, from, conversation_id, user, vxmlAttributes } = await this._initializeContext(callSid);
|
|
447
|
+
|
|
448
|
+
//send message to tiledesk
|
|
449
|
+
let tiledeskMessage = {
|
|
450
|
+
text: message_text,
|
|
451
|
+
senderFullname: from,
|
|
452
|
+
type: 'text',
|
|
453
|
+
channel: { name: CHANNEL_NAME },
|
|
454
|
+
attributes: attributes
|
|
455
|
+
};
|
|
456
|
+
let response = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
457
|
+
if (!response) {
|
|
458
|
+
return res.status(503).send({ message: "Bad response: Quota exceeded" })
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let start_time_get_message = new Date()
|
|
462
|
+
let message = await this._getMessage(callSid, from, project_id, conversation_id, this.tdChannel)
|
|
463
|
+
let end_time_get_message = new Date()
|
|
464
|
+
logger.verbose(`Time to getMessage from queue in /menublock/${callSid} : ${(end_time_get_message - start_time_get_message)}[ms]` + ' --- at time:' + new Date())
|
|
465
|
+
|
|
466
|
+
// convert response to vxml
|
|
467
|
+
let messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
468
|
+
logger.debug("(voice) VXML to SEND: " + messageToVXML);
|
|
469
|
+
|
|
470
|
+
let end_call = new Date().getTime();
|
|
471
|
+
logger.info(`Time to respond to /menublock/${callSid} : ${(end_call - start_call)} [ms]`)
|
|
472
|
+
|
|
473
|
+
res.set('Content-Type', 'application/xml');
|
|
474
|
+
res.status(200).send(messageToVXML);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
logger.error("(voice) Error in menublock handler:", error);
|
|
477
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async handleEvent(req, res) {
|
|
482
|
+
try {
|
|
483
|
+
logger.debug("(voice) called POST /handle", req.body);
|
|
484
|
+
logger.debug("(voice) called POST /handle query -->", req.query);
|
|
485
|
+
logger.debug("(voice) called POST /handle params-->", req.params);
|
|
486
|
+
|
|
487
|
+
let event = req.params.event;
|
|
488
|
+
let callSid = req.params.callSid;
|
|
489
|
+
let button_action = '#' + req.query.button_action;
|
|
490
|
+
let previousIntentName = req.query.intentName;
|
|
491
|
+
|
|
492
|
+
const { sessionInfo, project_id, from, conversation_id, user, vxmlAttributes } = await this._initializeContext(callSid);
|
|
493
|
+
|
|
494
|
+
//send message to tiledesk
|
|
495
|
+
let tiledeskMessage = {
|
|
496
|
+
text: "/" + event,
|
|
497
|
+
senderFullname: from,
|
|
498
|
+
type: 'text',
|
|
499
|
+
channel: { name: CHANNEL_NAME },
|
|
500
|
+
attributes: {
|
|
501
|
+
type: 'info',
|
|
502
|
+
action: button_action,
|
|
503
|
+
payload: {
|
|
504
|
+
event: event,
|
|
505
|
+
lastBlock: previousIntentName,
|
|
506
|
+
lastTimestamp: Date.now()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
let tdMessage = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
511
|
+
|
|
512
|
+
//generate Tiledesk wait message
|
|
513
|
+
let delayTime = await this.voiceChannel.getNextDelayTimeForCallId(callSid)
|
|
514
|
+
let message = await this.tdChannel.generateWaitTdMessage(callSid, delayTime)
|
|
515
|
+
///update delayIndex for wait command message time
|
|
516
|
+
await this.voiceChannel.saveDelayIndexForCallId(callSid)
|
|
517
|
+
|
|
518
|
+
// convert response to vxml
|
|
519
|
+
let messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
520
|
+
logger.debug("(voice) VXML to SEND: " + messageToVXML);
|
|
521
|
+
|
|
522
|
+
res.set('Content-Type', 'application/xml');
|
|
523
|
+
res.status(200).send(messageToVXML);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
logger.error("(voice) Error in handleEvent handler:", error);
|
|
526
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async event(req, res) {
|
|
531
|
+
try {
|
|
532
|
+
logger.debug("(voice) called POST /event", req.params);
|
|
533
|
+
logger.debug("(voice) called POST /event query", req.query);
|
|
534
|
+
logger.debug("(voice) called POST /event body", req.body);
|
|
535
|
+
|
|
536
|
+
let event = req.params.event;
|
|
537
|
+
let callSid = req.params.callSid;
|
|
538
|
+
let currentIntentName = req.query.intentName;
|
|
539
|
+
let currentIntentTimestamp = req.query.previousIntentTimestamp;
|
|
540
|
+
|
|
541
|
+
const { sessionInfo, project_id, from, conversation_id, user, vxmlAttributes } = await this._initializeContext(callSid);
|
|
542
|
+
|
|
543
|
+
let button_action = ''
|
|
544
|
+
if (event === 'transfer') {
|
|
545
|
+
let callStatus = req.body.CallStatus;
|
|
546
|
+
switch (callStatus) {
|
|
547
|
+
case CALL_STATUS.COMPLETED:
|
|
548
|
+
button_action = '#' + req.query.button_success;
|
|
549
|
+
break;
|
|
550
|
+
case CALL_STATUS.FAILED:
|
|
551
|
+
button_action = '#' + req.query.button_failure;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
switch (callStatus) {
|
|
556
|
+
case CALL_STATUS.COMPLETED:
|
|
557
|
+
case CALL_STATUS.FAILED: {
|
|
558
|
+
//send message to tiledesk
|
|
559
|
+
let tiledeskMessage = {
|
|
560
|
+
//text:'\\close',
|
|
561
|
+
text: '/' + event,
|
|
562
|
+
senderFullname: from,
|
|
563
|
+
type: 'text',
|
|
564
|
+
channel: { name: CHANNEL_NAME },
|
|
565
|
+
attributes: {
|
|
566
|
+
subtype: "info",
|
|
567
|
+
action: button_action,
|
|
568
|
+
payload: {
|
|
569
|
+
event: event,
|
|
570
|
+
lastBlock: currentIntentName,
|
|
571
|
+
lastTimestamp: currentIntentTimestamp
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
let tdMessage = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
//generate Tiledesk wait message
|
|
583
|
+
let delayTime = await this.voiceChannel.getNextDelayTimeForCallId(callSid)
|
|
584
|
+
let message = await this.tdChannel.generateWaitTdMessage(callSid, delayTime)
|
|
585
|
+
///update delayIndex for wait command message time
|
|
586
|
+
await this.voiceChannel.saveDelayIndexForCallId(callSid)
|
|
587
|
+
|
|
588
|
+
// convert response to vxml
|
|
589
|
+
let messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo)
|
|
590
|
+
logger.debug("(voice) VXML to SEND: " + messageToVXML);
|
|
591
|
+
|
|
592
|
+
res.set('Content-Type', 'application/xml');
|
|
593
|
+
res.status(200).send(messageToVXML);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
logger.error("(voice) Error in event handler:", error);
|
|
596
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async twilioStatus(req, res) {
|
|
601
|
+
try {
|
|
602
|
+
logger.debug('+++++++++++(voice) called POST twilio/status ', req.body);
|
|
603
|
+
|
|
604
|
+
let event = req.body.CallStatus;
|
|
605
|
+
let callSid = req.body.CallSid;
|
|
606
|
+
|
|
607
|
+
const { sessionInfo, project_id, from, conversation_id, user } = await this._initializeContext(callSid);
|
|
608
|
+
|
|
609
|
+
switch (event) {
|
|
610
|
+
case CALL_STATUS.COMPLETED: {
|
|
611
|
+
//send message to tiledesk
|
|
612
|
+
let tiledeskMessage = {
|
|
613
|
+
//text:'\\close',
|
|
614
|
+
text: '/close',
|
|
615
|
+
senderFullname: from,
|
|
616
|
+
type: 'text',
|
|
617
|
+
channel: { name: CHANNEL_NAME },
|
|
618
|
+
attributes: {
|
|
619
|
+
subtype: "info",
|
|
620
|
+
action: 'close' + JSON.stringify({ event: event }),
|
|
621
|
+
payload: {
|
|
622
|
+
catchEvent: event
|
|
623
|
+
},
|
|
624
|
+
timestamp: 'xxxxxx'
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
let tdMessage = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
629
|
+
|
|
630
|
+
//remove session data for current callId and relative queue data
|
|
631
|
+
await this.voiceChannel.deleteCallKeys(callSid);
|
|
632
|
+
await this.tdChannel.clearQueue(conversation_id);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
res.status(200).send();
|
|
638
|
+
} catch (error) {
|
|
639
|
+
logger.error("(voice) Error in twilioStatus handler:", error);
|
|
640
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async twilioFail(req, res) {
|
|
645
|
+
logger.debug('+++++++++++(voice) called POST twilio/fail ', req.params)
|
|
646
|
+
logger.debug('+++++++++++(voice) called POST twilio/fail ', req.body)
|
|
647
|
+
|
|
648
|
+
res.set('Content-Type', 'application/xml');
|
|
649
|
+
res.status(200).send('<Response></Response>');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async _generateSTT(audioFileUrl, attributes, sessionInfo, settings) {
|
|
653
|
+
|
|
654
|
+
logger.debug("(voice) generateSTT: " + attributes.VOICE_PROVIDER);
|
|
655
|
+
|
|
656
|
+
let tiledeskMessage = {};
|
|
657
|
+
let text = null;
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
switch (attributes.VOICE_PROVIDER) {
|
|
661
|
+
case VOICE_PROVIDER.OPENAI: {
|
|
662
|
+
let GPT_KEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.key
|
|
663
|
+
let publicKey = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.OPENAI))?.publicKey
|
|
664
|
+
if (publicKey) {
|
|
665
|
+
let keep_going = await this.aiService.checkQuoteAvailability(sessionInfo.project_id, settings.token)
|
|
666
|
+
logger.debug('(voice) checkQuoteAvailability return: ' + keep_going);
|
|
667
|
+
if (!keep_going) {
|
|
668
|
+
//no token is available --> close conversation
|
|
669
|
+
return tiledeskMessage = {
|
|
670
|
+
//text:'\\close',
|
|
671
|
+
text: '/close',
|
|
672
|
+
senderFullname: sessionInfo.from,
|
|
673
|
+
type: 'text',
|
|
674
|
+
channel: { name: CHANNEL_NAME },
|
|
675
|
+
attributes: {
|
|
676
|
+
subtype: "info",
|
|
677
|
+
action: 'close' + JSON.stringify({ event: 'quota_exceeded' }),
|
|
678
|
+
payload: {
|
|
679
|
+
catchEvent: 'quota_exceeded'
|
|
680
|
+
},
|
|
681
|
+
timestamp: 'xxxxxx'
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
text = await this.aiService.speechToText(audioFileUrl, attributes.STT_MODEL, GPT_KEY)
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case VOICE_PROVIDER.ELEVENLABS: {
|
|
692
|
+
let ELEVENLABS_APIKEY = sessionInfo.integrations.find((el => el.type === VOICE_PROVIDER.ELEVENLABS))?.key
|
|
693
|
+
const ttsLanguage = attributes.TTS_LANGUAGE || 'en';
|
|
694
|
+
text = await this.aiService.speechToTextElevenLabs(audioFileUrl, attributes.STT_MODEL, ttsLanguage, ELEVENLABS_APIKEY)
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
default:
|
|
698
|
+
throw new Error('Unsupported VOICE_PROVIDER: ' + attributes.VOICE_PROVIDER);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (text) {
|
|
702
|
+
logger.debug('[STT] text empty → fallback no_input');
|
|
703
|
+
tiledeskMessage = {
|
|
704
|
+
text: text,
|
|
705
|
+
senderFullname: sessionInfo.from,
|
|
706
|
+
type: 'text',
|
|
707
|
+
channel: { name: CHANNEL_NAME }
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logger.error('[STT] generateSTT error:', error);
|
|
712
|
+
switch (error.code) {
|
|
713
|
+
case 'AISERVICE_FAILED':
|
|
714
|
+
logger.error('[STT] AISERVICE_FAILED → ', error.message);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// fallback: tiledeskMessage vuoto
|
|
719
|
+
tiledeskMessage = {};
|
|
720
|
+
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return tiledeskMessage
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
_buildNoInputMessage(event, { from, button_action, payload }) {
|
|
727
|
+
return {
|
|
728
|
+
text: `/${event}`,
|
|
729
|
+
senderFullname: from,
|
|
730
|
+
type: 'text',
|
|
731
|
+
channel: { name: CHANNEL_NAME },
|
|
732
|
+
attributes: {
|
|
733
|
+
type: 'info',
|
|
734
|
+
action: button_action,
|
|
735
|
+
payload: payload
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async _getMessage(callSid, ani, project_id, conversation_id, tdChannel) {
|
|
741
|
+
const startTime = Date.now();
|
|
742
|
+
|
|
743
|
+
// Use the passed tdChannel instead of creating a new one
|
|
744
|
+
// const tdChannel = new TiledeskChannel({ ... })
|
|
745
|
+
// tdChannel.setProjectId(project_id);
|
|
746
|
+
|
|
747
|
+
let message = {}, queue = []
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
// 1. First attempt: read from queue
|
|
751
|
+
queue = await tdChannel.getMessagesFromQueue(conversation_id)
|
|
752
|
+
logger.debug('[getMessage] /NEXT check queue length--> ' + queue.length)
|
|
753
|
+
|
|
754
|
+
if (queue && queue.length > 0) {
|
|
755
|
+
message = queue[0]
|
|
756
|
+
logger.verbose('[getMessage] QUEUE --> ' + message.text)
|
|
757
|
+
|
|
758
|
+
await tdChannel.removeMessageFromQueue(conversation_id, message._id)
|
|
759
|
+
await this.voiceChannel.clearDelayTimeForCallId(callSid)
|
|
760
|
+
|
|
761
|
+
return message;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 2. If queue is empty: subscribe with timeout
|
|
765
|
+
if (queue && queue.length === 0) {
|
|
766
|
+
logger.debug("[getMessage] Queue is empty, starting subscription...");
|
|
767
|
+
|
|
768
|
+
let timeoutId;
|
|
769
|
+
|
|
770
|
+
const subscriptionPromise = (async () => {
|
|
771
|
+
await tdChannel.subscribeToTopic(conversation_id);
|
|
772
|
+
queue = await tdChannel.getMessagesFromQueue(conversation_id)
|
|
773
|
+
|
|
774
|
+
if (!queue || queue.length === 0) {
|
|
775
|
+
throw new Error("No message received after subscription");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
message = queue[0]
|
|
779
|
+
logger.verbose(`[getMessage] Message received from subscription: ${message.text}`);
|
|
780
|
+
|
|
781
|
+
await tdChannel.removeMessageFromQueue(conversation_id, message._id)
|
|
782
|
+
await this.voiceChannel.clearDelayTimeForCallId(callSid)
|
|
783
|
+
|
|
784
|
+
if (timeoutId) clearTimeout(timeoutId); // Clear timeout if subscription wins
|
|
785
|
+
return message;
|
|
786
|
+
})();
|
|
787
|
+
|
|
788
|
+
const timeoutPromise = new Promise(async (resolve) => {
|
|
789
|
+
timeoutId = setTimeout(async () => {
|
|
790
|
+
logger.debug("[getMessage] Subscription timeout, generating waitTdMessage...");
|
|
791
|
+
|
|
792
|
+
//CASE: queue is empty --> generate Tiledesk wait message and manage delayTime
|
|
793
|
+
const delayTime = await this.voiceChannel.getNextDelayTimeForCallId(callSid);
|
|
794
|
+
const waitMessage = await tdChannel.generateWaitTdMessage(ani, delayTime);
|
|
795
|
+
//update delayIndex for wait command message time
|
|
796
|
+
await this.voiceChannel.saveDelayIndexForCallId(callSid);
|
|
797
|
+
|
|
798
|
+
resolve(waitMessage);
|
|
799
|
+
}, this.config.MAX_POLLING_TIME * 1000);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
return await Promise.race([subscriptionPromise, timeoutPromise]);
|
|
803
|
+
}
|
|
804
|
+
} catch (err) {
|
|
805
|
+
logger.error("[getMessage] Error:", err);
|
|
806
|
+
throw err;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
module.exports = VoiceController;
|