@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,509 @@
1
+ /**
2
+ * TiledeskTwilioTranslator - Refactored Version
3
+ *
4
+ * Uses Twilio's VoiceResponse for TwiML generation instead of manual XML.
5
+ * Benefits: Type safety, auto-validation, better error messages.
6
+ */
7
+
8
+ const { twiml } = require('twilio');
9
+ const querystring = require('querystring');
10
+
11
+ const logger = require('../../utils/logger');
12
+ const utils = require('../../utils/utils');
13
+ const utilsMessage = require('../../utils/utils-message');
14
+
15
+ const {
16
+ SETTING_MESSAGE,
17
+ WAIT_MESSAGE,
18
+ TEXT_MESSAGE,
19
+ VOICE_PROVIDER,
20
+ OPENAI_SETTINGS,
21
+ ELEVENLABS_SETTINGS,
22
+ TYPE_ACTION_VXML,
23
+ TYPE_MESSAGE,
24
+ INFO_MESSAGE_TYPE
25
+ } = require('../../utils/constants');
26
+
27
+ const voiceEventEmitter = require('../voiceEventEmitter');
28
+ const { SttError } = require('../../utils/errors');
29
+
30
+ class TiledeskTwilioTranslator {
31
+ constructor(config) {
32
+ if (!config.BASE_URL) {
33
+ throw new Error('[TiledeskTwilioTranslator] config.BASE_URL is mandatory');
34
+ }
35
+
36
+ this.BASE_URL = config.BASE_URL;
37
+ this.aiService = config.aiService;
38
+ this.uploadService = config.uploadService;
39
+ this.lastCallSidVerb = {};
40
+ this.voiceProvider = VOICE_PROVIDER.TWILIO;
41
+ }
42
+
43
+ /**
44
+ * Main entry point: Convert Tiledesk message to TwiML
45
+ */
46
+ async toVXML(msg, id, vxmlAttributes, sessionInfo) {
47
+ vxmlAttributes.intentName = msg.attributes?.intentName || '';
48
+ vxmlAttributes = this._processVoiceAttributes(msg, vxmlAttributes);
49
+
50
+ // Handle close/hangup
51
+ if (this._shouldHangup(msg)) {
52
+ return this._createHangupResponse();
53
+ }
54
+
55
+ const commands = msg.attributes?.commands;
56
+ if (!commands?.length) {
57
+ return this._createEmptyResponse();
58
+ }
59
+
60
+ // Set VXML attributes from commands
61
+ this._setAttributesFromCommands(commands, vxmlAttributes);
62
+
63
+ // Route to appropriate converter based on message type
64
+ return this._routeToConverter(msg, vxmlAttributes, sessionInfo);
65
+ }
66
+
67
+ /**
68
+ * Route message to the appropriate TwiML converter
69
+ */
70
+ async _routeToConverter(msg, vxmlAttributes, sessionInfo) {
71
+ const converters = [
72
+ { check: () => this._isWaitMessage(msg), convert: () => this._convertDelay(msg, vxmlAttributes, sessionInfo) },
73
+ { check: () => this._hasSubType(msg, TYPE_ACTION_VXML.DTMF_FORM), convert: () => this._convertDtmfForm(msg, vxmlAttributes, sessionInfo) },
74
+ { check: () => this._hasSubType(msg, TYPE_ACTION_VXML.BLIND_TRANSFER), convert: () => this._convertBlindTransfer(msg, vxmlAttributes, sessionInfo) },
75
+ { check: () => this._hasSubType(msg, TYPE_ACTION_VXML.DTMF_MENU), convert: () => this._convertMenu(msg, vxmlAttributes, sessionInfo) },
76
+ { check: () => this._hasSubType(msg, TYPE_ACTION_VXML.SPEECH_FORM), convert: () => this._convertSpeechForm(msg, vxmlAttributes, sessionInfo) },
77
+ ];
78
+
79
+ for (const { check, convert } of converters) {
80
+ if (check()) {
81
+ return convert();
82
+ }
83
+ }
84
+
85
+ // Default: Play prompt
86
+ return this._convertPlayPrompt(msg, vxmlAttributes, sessionInfo);
87
+ }
88
+
89
+ // ==================== Message Type Checks ====================
90
+
91
+ _shouldHangup(msg) {
92
+ const isInfoSupport = utilsMessage.messageType(TYPE_MESSAGE.INFO_SUPPORT, msg);
93
+ return isInfoSupport && utilsMessage.infoMessageType(msg) === INFO_MESSAGE_TYPE.CHAT_CLOSED;
94
+ }
95
+
96
+ _hasSubType(msg, subType) {
97
+ const settings = msg.attributes?.commands?.find(cmd => cmd.type === SETTING_MESSAGE);
98
+ return settings?.subType === subType;
99
+ }
100
+
101
+ _isWaitMessage(msg) {
102
+ const commands = msg.attributes?.commands;
103
+ if (!commands) return false;
104
+ const hasWait = commands.some(cmd => cmd.type === WAIT_MESSAGE);
105
+ const hasMessage = commands.some(cmd => cmd.type === TEXT_MESSAGE);
106
+ return hasWait && !hasMessage;
107
+ }
108
+
109
+ // ==================== Voice Attributes Management ====================
110
+
111
+ _processVoiceAttributes(msg, vxmlAttributes) {
112
+ const flowAttrs = msg.attributes?.flowAttributes;
113
+ if (!flowAttrs) {
114
+ voiceEventEmitter.emit('saveSettings', vxmlAttributes);
115
+ return vxmlAttributes;
116
+ }
117
+
118
+ // Copy flow attributes to vxml attributes
119
+ Object.keys(vxmlAttributes).forEach(key => {
120
+ if (flowAttrs[key]) {
121
+ vxmlAttributes[key] = flowAttrs[key];
122
+ }
123
+ });
124
+
125
+ // Set voice provider with defaults
126
+ this.voiceProvider = flowAttrs.VOICE_PROVIDER || VOICE_PROVIDER.TWILIO;
127
+ vxmlAttributes.VOICE_PROVIDER = this.voiceProvider;
128
+
129
+ // Apply provider-specific defaults
130
+ this._applyProviderDefaults(vxmlAttributes, flowAttrs);
131
+
132
+ logger.debug('[TiledeskTwilioTranslator] Processed vxmlAttributes:', vxmlAttributes);
133
+ voiceEventEmitter.emit('saveSettings', vxmlAttributes);
134
+
135
+ return vxmlAttributes;
136
+ }
137
+
138
+ _applyProviderDefaults(attrs, flowAttrs) {
139
+ const providerDefaults = {
140
+ [VOICE_PROVIDER.OPENAI]: OPENAI_SETTINGS,
141
+ [VOICE_PROVIDER.ELEVENLABS]: ELEVENLABS_SETTINGS
142
+ };
143
+
144
+ const defaults = providerDefaults[this.voiceProvider];
145
+ if (!defaults) return;
146
+
147
+ attrs.TTS_VOICE_NAME = flowAttrs.TTS_VOICE_NAME || defaults.TTS_VOICE_NAME;
148
+ attrs.TTS_MODEL = flowAttrs.TTS_MODEL || defaults.TTS_MODEL;
149
+ attrs.STT_MODEL = flowAttrs.STT_MODEL || defaults.STT_MODEL;
150
+
151
+ if (this.voiceProvider === VOICE_PROVIDER.ELEVENLABS) {
152
+ attrs.TTS_VOICE_LANGUAGE = flowAttrs.TTS_VOICE_LANGUAGE || defaults.TTS_VOICE_LANGUAGE;
153
+ }
154
+ }
155
+
156
+ _setAttributesFromCommands(commands, attributes) {
157
+ const settingsCommand = commands.slice(-1)[0];
158
+ if (settingsCommand?.settings) {
159
+ Object.assign(attributes, settingsCommand.settings);
160
+ }
161
+ }
162
+
163
+ // ==================== TwiML Converters ====================
164
+
165
+ _createHangupResponse() {
166
+ const response = new twiml.VoiceResponse();
167
+ response.hangup();
168
+ return response.toString();
169
+ }
170
+
171
+ _createEmptyResponse() {
172
+ const response = new twiml.VoiceResponse();
173
+ return response.toString();
174
+ }
175
+
176
+ async _convertDelay(msg, attrs, sessionInfo) {
177
+ const response = new twiml.VoiceResponse();
178
+ await this._addPromptElements(response, msg, attrs, sessionInfo);
179
+ response.redirect({ method: 'POST' }, `${this.BASE_URL}/nextblock/${attrs.callSid}`);
180
+ return response.toString();
181
+ }
182
+
183
+ async _convertPlayPrompt(msg, attrs, sessionInfo) {
184
+ const response = new twiml.VoiceResponse();
185
+ const settings = msg.attributes?.commands?.find(cmd => cmd.type === SETTING_MESSAGE)?.settings;
186
+ const bargeIn = settings?.bargein === true;
187
+
188
+ if (bargeIn) {
189
+ // Wrap prompt in Gather to capture user input when barge-in occurs
190
+ const intentQuery = this._buildIntentQuery(attrs);
191
+ const bargeInInput = 'speech dtmf'; // Default to both
192
+ const noInputNoMatch = this._getNoInputNoMatchActions(msg);
193
+ const supportsSpeech = bargeInInput.includes('speech');
194
+
195
+ const gatherOptions = {
196
+ input: bargeInInput,
197
+ action: `${this.BASE_URL}/nextBlock/${attrs.callSid}?${intentQuery}&bargein=true`,
198
+ method: 'POST',
199
+ language: attrs.TTS_VOICE_LANGUAGE
200
+ };
201
+
202
+ // Apply speech-specific options only if speech is enabled
203
+ if (supportsSpeech) {
204
+ gatherOptions.speechTimeout = 'auto';
205
+ gatherOptions.enhanced = true;
206
+ if (attrs.incompleteSpeechTimeout) {
207
+ gatherOptions.speechTimeout = Math.round(attrs.incompleteSpeechTimeout / 1000);
208
+ }
209
+ }
210
+
211
+ // Apply timeout if specified (for DTMF or as fallback)
212
+ if (attrs.noInputTimeout) {
213
+ gatherOptions.timeout = Math.round(attrs.noInputTimeout / 1000);
214
+ }
215
+
216
+ const gather = response.gather(gatherOptions);
217
+ await this._addPromptElements(gather, msg, attrs, sessionInfo, true); // Pass bargeIn flag
218
+
219
+ // Handle no input/no match scenarios
220
+ if (noInputNoMatch?.queryNoInput) {
221
+ response.redirect({ method: 'POST' },
222
+ `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
223
+ }
224
+ } else {
225
+ // No barge-in: simple play prompt
226
+ await this._addPromptElements(response, msg, attrs, sessionInfo, false);
227
+ response.redirect({ method: 'POST' }, `${this.BASE_URL}/nextblock/${attrs.callSid}`);
228
+ }
229
+
230
+ return response.toString();
231
+ }
232
+
233
+ async _convertSpeechForm(msg, attrs, sessionInfo) {
234
+ const response = new twiml.VoiceResponse();
235
+ const intentQuery = this._buildIntentQuery(attrs);
236
+ const noInputNoMatch = this._getNoInputNoMatchActions(msg);
237
+
238
+ if (this.voiceProvider === VOICE_PROVIDER.TWILIO) {
239
+ const gatherOptions = {
240
+ input: 'speech',
241
+ action: `${this.BASE_URL}/nextBlock/${attrs.callSid}?${intentQuery}`,
242
+ method: 'POST',
243
+ language: attrs.TTS_VOICE_LANGUAGE,
244
+ speechTimeout: 'auto',
245
+ enhanced: true
246
+ };
247
+
248
+ if (attrs.incompleteSpeechTimeout) {
249
+ gatherOptions.speechTimeout = Math.round(attrs.incompleteSpeechTimeout / 1000);
250
+ }
251
+
252
+ const gather = response.gather(gatherOptions);
253
+ await this._addPromptElements(gather, msg, attrs, sessionInfo);
254
+
255
+ if (noInputNoMatch?.queryNoInput) {
256
+ response.redirect({ method: 'POST' },
257
+ `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
258
+ }
259
+ } else {
260
+ // OpenAI/ElevenLabs: Use Record
261
+ await this._addPromptElements(response, msg, attrs, sessionInfo);
262
+
263
+ let queryUrl = intentQuery;
264
+ if (noInputNoMatch?.queryNoInput) {
265
+ queryUrl += `&${noInputNoMatch.queryNoInput}`;
266
+ }
267
+
268
+ response.record({
269
+ action: `${this.BASE_URL}/record/action/${attrs.callSid}?${queryUrl}`,
270
+ method: 'POST',
271
+ playBeep: false,
272
+ trim: 'trim-silence',
273
+ timeout: 2,
274
+ recordingStatusCallback: `${this.BASE_URL}/record/callback/${attrs.callSid}?${queryUrl}`,
275
+ recordingStatusCallbackMethod: 'POST'
276
+ });
277
+ }
278
+
279
+ return response.toString();
280
+ }
281
+
282
+ async _convertMenu(msg, attrs, sessionInfo) {
283
+ const response = new twiml.VoiceResponse();
284
+ const options = this._getButtonsFromMessage(msg);
285
+ const menuOptions = options.map(o => `${o.value}:${o.action.substring(1)}`).join(';') + ';';
286
+ const noInputNoMatch = this._getNoInputNoMatchActions(msg);
287
+
288
+ let queryUrl = `${this._buildIntentQuery(attrs)}&menu_options=${menuOptions}`;
289
+ if (noInputNoMatch?.queryNoMatch) {
290
+ queryUrl += `&${noInputNoMatch.queryNoMatch}`;
291
+ }
292
+
293
+ const gather = response.gather({
294
+ input: 'dtmf',
295
+ timeout: Math.round(attrs.noInputTimeout / 1000),
296
+ numDigits: 1,
297
+ action: `${this.BASE_URL}/menublock/${attrs.callSid}?${queryUrl}`,
298
+ method: 'POST',
299
+ language: attrs.TTS_VOICE_LANGUAGE
300
+ });
301
+
302
+ await this._addPromptElements(gather, msg, attrs, sessionInfo);
303
+
304
+ if (noInputNoMatch?.queryNoInput) {
305
+ response.redirect({},
306
+ `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
307
+ }
308
+
309
+ return response.toString();
310
+ }
311
+
312
+ async _convertDtmfForm(msg, attrs, sessionInfo) {
313
+ const response = new twiml.VoiceResponse();
314
+ const noInputNoMatch = this._getNoInputNoMatchActions(msg);
315
+ const intentQuery = this._buildIntentQuery(attrs);
316
+
317
+ const gatherOptions = {
318
+ input: 'dtmf',
319
+ timeout: Math.round(attrs.noInputTimeout / 1000),
320
+ action: `${this.BASE_URL}/menublock/${attrs.callSid}?${intentQuery}`,
321
+ method: 'POST',
322
+ language: attrs.TTS_VOICE_LANGUAGE
323
+ };
324
+
325
+ // Apply DTMF-specific settings
326
+ this._applyDtmfSettings(msg, gatherOptions, attrs);
327
+
328
+ const gather = response.gather(gatherOptions);
329
+ await this._addPromptElements(gather, msg, attrs, sessionInfo);
330
+
331
+ if (noInputNoMatch?.queryNoInput) {
332
+ response.redirect({ method: 'POST' },
333
+ `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
334
+ }
335
+
336
+ return response.toString();
337
+ }
338
+
339
+ async _convertBlindTransfer(msg, attrs, sessionInfo) {
340
+ const response = new twiml.VoiceResponse();
341
+ await this._addPromptElements(response, msg, attrs, sessionInfo);
342
+
343
+ const lastCommand = msg.attributes.commands.slice(-1)[0];
344
+ const settings = lastCommand?.settings;
345
+
346
+ if (!settings?.transferTo) {
347
+ return response.toString();
348
+ }
349
+
350
+ const dial = response.dial();
351
+ let queryUrl = this._buildIntentQuery(attrs);
352
+
353
+ if (settings.trueIntent) {
354
+ queryUrl += `&button_success=${settings.trueIntent.substring(1)}`;
355
+ }
356
+ if (settings.falseIntent) {
357
+ queryUrl += `&button_failure=${settings.falseIntent.substring(1)}`;
358
+ }
359
+
360
+ const isPhoneNumber = /^.*\d.*$/.test(settings.transferTo);
361
+ if (isPhoneNumber) {
362
+ dial.number({
363
+ statusCallbackEvent: 'initiated ringing answered completed',
364
+ statusCallback: `${this.BASE_URL}/event/${attrs.callSid}/transfer?${queryUrl}`,
365
+ statusCallbackMethod: 'POST'
366
+ }, settings.transferTo);
367
+ } else {
368
+ dial.sip({}, `sip:${settings.transferTo}${attrs.uriTransferParameters || ''}`);
369
+ }
370
+
371
+ return response.toString();
372
+ }
373
+
374
+ // ==================== Prompt Elements ====================
375
+
376
+ async _addPromptElements(element, msg, attrs, sessionInfo, bargeIn = false) {
377
+ const commands = msg.attributes?.commands || [];
378
+
379
+ for (const command of commands) {
380
+ if (command.type === 'message') {
381
+ await this._addMessageElement(element, command, attrs, sessionInfo, bargeIn);
382
+ } else if (command.type === 'wait' && command.time > 0) {
383
+ element.pause({ length: command.time / 1000 });
384
+ this.lastCallSidVerb[attrs.callSid] = 'pause';
385
+ }
386
+ }
387
+ }
388
+
389
+ async _addMessageElement(element, command, attrs, sessionInfo, bargeIn = false) {
390
+ const { message } = command;
391
+
392
+ if (message.type === 'text') {
393
+ const text = utils.markdownToTwilioSpeech(message.text);
394
+
395
+ if (this.voiceProvider !== VOICE_PROVIDER.TWILIO) {
396
+ const audioUrl = await this._generateTTS(text, attrs, sessionInfo);
397
+ if (audioUrl) {
398
+ const playOptions = {};
399
+ if (bargeIn) {
400
+ playOptions.bargeIn = true;
401
+ }
402
+ element.play(playOptions, audioUrl);
403
+ }
404
+ } else {
405
+ const sayOptions = { voice: attrs.TTS_VOICE_NAME, language: attrs.TTS_VOICE_LANGUAGE };
406
+ if (bargeIn) {
407
+ sayOptions.bargeIn = true;
408
+ }
409
+ element.say(sayOptions, text);
410
+ this.lastCallSidVerb[attrs.callSid] = 'say';
411
+ }
412
+ } else if (message.type === 'frame' && message.metadata?.src) {
413
+
414
+ const playOptions = { loop: 0 };
415
+ if (bargeIn) {
416
+ playOptions.bargeIn = true;
417
+ }
418
+ element.play(playOptions, message.metadata.src);
419
+ this.lastCallSidVerb[attrs.callSid] = 'play';
420
+ }
421
+ }
422
+
423
+ // ==================== TTS Generation ====================
424
+
425
+ async _generateTTS(text, attrs, sessionInfo) {
426
+ try {
427
+ const audioData = await this._callTTSProvider(text, attrs, sessionInfo);
428
+
429
+ if (!audioData) {
430
+ throw new SttError('TTS_FAILED', 'TTS returned no audio data');
431
+ }
432
+
433
+ const fileUrl = await this.uploadService.upload(attrs.callSid, audioData, sessionInfo.user);
434
+ logger.debug('[TiledeskTwilioTranslator] TTS audio URL:', fileUrl);
435
+ return fileUrl;
436
+ } catch (error) {
437
+ logger.error('[TiledeskTwilioTranslator] TTS generation error:', error);
438
+ return null;
439
+ }
440
+ }
441
+
442
+ async _callTTSProvider(text, attrs, sessionInfo) {
443
+ const providers = {
444
+ [VOICE_PROVIDER.OPENAI]: () => {
445
+ const key = sessionInfo.integrations.find(i => i.type === VOICE_PROVIDER.OPENAI)?.key;
446
+ return this.aiService.textToSpeech(text, attrs.TTS_VOICE_NAME, attrs.TTS_MODEL, key);
447
+ },
448
+ [VOICE_PROVIDER.ELEVENLABS]: () => {
449
+ const key = sessionInfo.integrations.find(i => i.type === VOICE_PROVIDER.ELEVENLABS)?.key;
450
+ return this.aiService.textToSpeechElevenLabs(
451
+ text, attrs.TTS_VOICE_NAME, attrs.TTS_MODEL, attrs.TTS_VOICE_LANGUAGE, key
452
+ );
453
+ }
454
+ };
455
+
456
+ const provider = providers[this.voiceProvider];
457
+ if (!provider) {
458
+ throw new SttError('TTS_FAILED', `Unsupported voice provider: ${this.voiceProvider}`);
459
+ }
460
+
461
+ return provider();
462
+ }
463
+
464
+ // ==================== Helpers ====================
465
+
466
+ _buildIntentQuery(attrs) {
467
+ return `intentName=${querystring.encode(attrs.intentName)}&previousIntentTimestamp=${Date.now()}`;
468
+ }
469
+
470
+ _getNoInputNoMatchActions(msg) {
471
+ const settings = msg.attributes?.commands?.find(cmd => cmd.type === SETTING_MESSAGE)?.settings;
472
+ if (!settings) return null;
473
+
474
+ const { noInputIntent, noMatchIntent } = settings;
475
+ return {
476
+ queryNoInput: noInputIntent ? `button_action=${noInputIntent.substring(1)}` : '',
477
+ queryNoMatch: noMatchIntent ? `button_action=${noMatchIntent.substring(1)}` : ''
478
+ };
479
+ }
480
+
481
+ _getButtonsFromMessage(msg) {
482
+ const command = msg.attributes?.commands?.find(cmd =>
483
+ cmd.type === 'message' && cmd.message?.attributes?.attachment?.buttons
484
+ );
485
+ const buttons = command?.message?.attributes?.attachment?.buttons || [];
486
+ return buttons.filter(b => b.type === 'action');
487
+ }
488
+
489
+ _applyDtmfSettings(msg, gatherOptions, attrs) {
490
+ const settings = msg.attributes?.commands?.find(cmd => cmd.type === SETTING_MESSAGE)?.settings;
491
+ if (!settings) return;
492
+
493
+ if (settings.maxDigits) {
494
+ gatherOptions.numDigits = settings.maxDigits;
495
+ attrs.maxDigits = settings.maxDigits;
496
+ }
497
+ if (settings.terminators) {
498
+ gatherOptions.finishOnKey = settings.terminators;
499
+ attrs.terminators = settings.terminators;
500
+ }
501
+ }
502
+
503
+ // Legacy method for backwards compatibility
504
+ toTiledesk(vxmlMessage) {
505
+ logger.debug('[TiledeskTwilioTranslator] toTiledesk:', vxmlMessage);
506
+ }
507
+ }
508
+
509
+ module.exports = { TiledeskTwilioTranslator };