@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.
Files changed (50) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +44 -0
  3. package/index.js +7 -1562
  4. package/package.json +23 -22
  5. package/src/app.js +146 -0
  6. package/src/config/index.js +32 -0
  7. package/src/controllers/VoiceController.js +488 -0
  8. package/src/controllers/VoiceController.original.js +811 -0
  9. package/src/middlewares/httpLogger.js +31 -0
  10. package/src/models/KeyValueStore.js +78 -0
  11. package/src/routes/manageApp.js +298 -0
  12. package/src/routes/voice.js +22 -0
  13. package/src/services/AiService.js +219 -0
  14. package/src/services/AiService.sdk.js +367 -0
  15. package/src/services/IntegrationService.js +74 -0
  16. package/src/services/MessageService.js +133 -0
  17. package/src/services/README_SDK.md +107 -0
  18. package/src/services/SessionService.js +143 -0
  19. package/src/services/SpeechService.js +134 -0
  20. package/src/services/TiledeskMessageBuilder.js +135 -0
  21. package/src/services/TwilioService.js +122 -0
  22. package/src/services/UploadService.js +78 -0
  23. package/src/services/channels/TiledeskChannel.js +269 -0
  24. package/{tiledesk → src/services/channels}/VoiceChannel.js +17 -56
  25. package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
  26. package/src/services/index.js +45 -0
  27. package/src/services/translators/TiledeskTwilioTranslator.js +509 -0
  28. package/{tiledesk/TiledeskTwilioTranslator.js → src/services/translators/TiledeskTwilioTranslator.original.js} +119 -212
  29. package/src/utils/fileUtils.js +24 -0
  30. package/src/utils/logger.js +32 -0
  31. package/{tiledesk → src/utils}/utils-message.js +6 -21
  32. package/logs/app.log +0 -3082
  33. package/routes/manageApp.js +0 -419
  34. package/tiledesk/KVBaseMongo.js +0 -101
  35. package/tiledesk/TiledeskChannel.js +0 -363
  36. package/tiledesk/TiledeskSubscriptionClient.js +0 -135
  37. package/tiledesk/fileUtils.js +0 -55
  38. package/tiledesk/services/AiService.js +0 -230
  39. package/tiledesk/services/IntegrationService.js +0 -81
  40. package/tiledesk/services/UploadService.js +0 -88
  41. /package/{winston.js → src/config/logger.js} +0 -0
  42. /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
  43. /package/{template → src/template}/configure.html +0 -0
  44. /package/{template → src/template}/css/configure.css +0 -0
  45. /package/{template → src/template}/css/error.css +0 -0
  46. /package/{template → src/template}/css/style.css +0 -0
  47. /package/{template → src/template}/error.html +0 -0
  48. /package/{tiledesk → src/utils}/constants.js +0 -0
  49. /package/{tiledesk → src/utils}/errors.js +0 -0
  50. /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;