@tiledesk/tiledesk-voice-twilio-connector 0.1.28 → 0.2.0-rc6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +515 -0
  3. package/index.js +7 -1562
  4. package/package.json +23 -21
  5. package/src/app.js +154 -0
  6. package/src/config/index.js +32 -0
  7. package/src/controllers/VoiceController.js +493 -0
  8. package/src/middlewares/httpLogger.js +43 -0
  9. package/src/models/KeyValueStore.js +78 -0
  10. package/src/routes/manageApp.js +298 -0
  11. package/src/routes/voice.js +22 -0
  12. package/src/services/AiService.js +219 -0
  13. package/src/services/AiService.sdk.js +367 -0
  14. package/src/services/IntegrationService.js +74 -0
  15. package/src/services/MessageService.js +139 -0
  16. package/src/services/README_SDK.md +107 -0
  17. package/src/services/SessionService.js +143 -0
  18. package/src/services/SpeechService.js +134 -0
  19. package/src/services/TiledeskMessageBuilder.js +135 -0
  20. package/src/services/TwilioService.js +129 -0
  21. package/src/services/UploadService.js +78 -0
  22. package/src/services/channels/TiledeskChannel.js +268 -0
  23. package/{tiledesk → src/services/channels}/VoiceChannel.js +20 -59
  24. package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
  25. package/src/services/index.js +45 -0
  26. package/src/services/translators/TiledeskTwilioTranslator.js +514 -0
  27. package/src/utils/fileUtils.js +24 -0
  28. package/src/utils/logger.js +32 -0
  29. package/{tiledesk → src/utils}/utils-message.js +6 -21
  30. package/logs/app.log +0 -3082
  31. package/routes/manageApp.js +0 -419
  32. package/tiledesk/KVBaseMongo.js +0 -101
  33. package/tiledesk/TiledeskChannel.js +0 -363
  34. package/tiledesk/TiledeskSubscriptionClient.js +0 -135
  35. package/tiledesk/TiledeskTwilioTranslator.js +0 -707
  36. package/tiledesk/fileUtils.js +0 -55
  37. package/tiledesk/services/AiService.js +0 -230
  38. package/tiledesk/services/IntegrationService.js +0 -81
  39. package/tiledesk/services/UploadService.js +0 -88
  40. /package/{winston.js → src/config/logger.js} +0 -0
  41. /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
  42. /package/{template → src/template}/configure.html +0 -0
  43. /package/{template → src/template}/css/configure.css +0 -0
  44. /package/{template → src/template}/css/error.css +0 -0
  45. /package/{template → src/template}/css/style.css +0 -0
  46. /package/{template → src/template}/error.html +0 -0
  47. /package/{tiledesk → src/utils}/constants.js +0 -0
  48. /package/{tiledesk → src/utils}/errors.js +0 -0
  49. /package/{tiledesk → src/utils}/utils.js +0 -0
@@ -0,0 +1,514 @@
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
+ if (!response.toString().trim().includes('<Play loop="300">')) {
228
+ response.redirect({ method: 'POST' }, `${this.BASE_URL}/nextblock/${attrs.callSid}`);
229
+ }
230
+ }
231
+
232
+ return response.toString();
233
+ }
234
+
235
+ async _convertSpeechForm(msg, attrs, sessionInfo) {
236
+ const response = new twiml.VoiceResponse();
237
+ const intentQuery = this._buildIntentQuery(attrs);
238
+ const noInputNoMatch = this._getNoInputNoMatchActions(msg);
239
+
240
+ if (this.voiceProvider === VOICE_PROVIDER.TWILIO) {
241
+ const gatherOptions = {
242
+ input: 'speech',
243
+ action: `${this.BASE_URL}/nextBlock/${attrs.callSid}?${intentQuery}`,
244
+ method: 'POST',
245
+ language: attrs.TTS_VOICE_LANGUAGE,
246
+ speechTimeout: 'auto',
247
+ enhanced: true
248
+ };
249
+
250
+ if (attrs.incompleteSpeechTimeout) {
251
+ gatherOptions.speechTimeout = Math.round(attrs.incompleteSpeechTimeout / 1000);
252
+ }
253
+
254
+ const gather = response.gather(gatherOptions);
255
+ await this._addPromptElements(gather, msg, attrs, sessionInfo);
256
+
257
+ if (noInputNoMatch?.queryNoInput) {
258
+ response.redirect({ method: 'POST' },
259
+ `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
260
+ }
261
+ } else {
262
+ // OpenAI/ElevenLabs: Use Record
263
+ await this._addPromptElements(response, msg, attrs, sessionInfo);
264
+
265
+ let queryUrl = intentQuery;
266
+ if (noInputNoMatch?.queryNoInput) {
267
+ queryUrl += `&${noInputNoMatch.queryNoInput}`;
268
+ }
269
+
270
+ response.record({
271
+ action: `${this.BASE_URL}/record/action/${attrs.callSid}?${queryUrl}`,
272
+ method: 'POST',
273
+ playBeep: false,
274
+ trim: 'trim-silence',
275
+ timeout: 2,
276
+ recordingStatusCallback: `${this.BASE_URL}/record/callback/${attrs.callSid}?${queryUrl}`,
277
+ recordingStatusCallbackMethod: 'POST'
278
+ });
279
+ }
280
+
281
+ return response.toString();
282
+ }
283
+
284
+ async _convertMenu(msg, attrs, sessionInfo) {
285
+ const response = new twiml.VoiceResponse();
286
+ const options = this._getButtonsFromMessage(msg);
287
+ const menuOptions = options.map(o => `${o.value}:${o.action.substring(1)}`).join(';') + ';';
288
+ const noInputNoMatch = this._getNoInputNoMatchActions(msg);
289
+
290
+ let queryUrl = `${this._buildIntentQuery(attrs)}&menu_options=${menuOptions}`;
291
+ if (noInputNoMatch?.queryNoMatch) {
292
+ queryUrl += `&${noInputNoMatch.queryNoMatch}`;
293
+ }
294
+
295
+ const gather = response.gather({
296
+ input: 'dtmf',
297
+ timeout: Math.round(attrs.noInputTimeout / 1000),
298
+ numDigits: 1,
299
+ action: `${this.BASE_URL}/menublock/${attrs.callSid}?${queryUrl}`,
300
+ method: 'POST',
301
+ language: attrs.TTS_VOICE_LANGUAGE
302
+ });
303
+
304
+ await this._addPromptElements(gather, msg, attrs, sessionInfo);
305
+
306
+ if (noInputNoMatch?.queryNoInput) {
307
+ response.redirect({},
308
+ `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
309
+ }
310
+
311
+ return response.toString();
312
+ }
313
+
314
+ async _convertDtmfForm(msg, attrs, sessionInfo) {
315
+ const response = new twiml.VoiceResponse();
316
+ const noInputNoMatch = this._getNoInputNoMatchActions(msg);
317
+ const intentQuery = this._buildIntentQuery(attrs);
318
+
319
+ const gatherOptions = {
320
+ input: 'dtmf',
321
+ timeout: Math.round(attrs.noInputTimeout / 1000),
322
+ action: `${this.BASE_URL}/menublock/${attrs.callSid}?${intentQuery}`,
323
+ method: 'POST',
324
+ language: attrs.TTS_VOICE_LANGUAGE
325
+ };
326
+
327
+ // Apply DTMF-specific settings
328
+ this._applyDtmfSettings(msg, gatherOptions, attrs);
329
+
330
+ const gather = response.gather(gatherOptions);
331
+ await this._addPromptElements(gather, msg, attrs, sessionInfo);
332
+
333
+ if (noInputNoMatch?.queryNoInput) {
334
+ response.redirect({ method: 'POST' },
335
+ `${this.BASE_URL}/handle/${attrs.callSid}/no_input?${noInputNoMatch.queryNoInput}`);
336
+ }
337
+
338
+ return response.toString();
339
+ }
340
+
341
+ async _convertBlindTransfer(msg, attrs, sessionInfo) {
342
+ const response = new twiml.VoiceResponse();
343
+ await this._addPromptElements(response, msg, attrs, sessionInfo);
344
+
345
+ const lastCommand = msg.attributes.commands.slice(-1)[0];
346
+ const settings = lastCommand?.settings;
347
+
348
+ if (!settings?.transferTo) {
349
+ return response.toString();
350
+ }
351
+
352
+ const dial = response.dial();
353
+ let queryUrl = this._buildIntentQuery(attrs);
354
+
355
+ if (settings.trueIntent) {
356
+ queryUrl += `&button_success=${settings.trueIntent.substring(1)}`;
357
+ }
358
+ if (settings.falseIntent) {
359
+ queryUrl += `&button_failure=${settings.falseIntent.substring(1)}`;
360
+ }
361
+
362
+ const isPhoneNumber = /^.*\d.*$/.test(settings.transferTo);
363
+ if (isPhoneNumber) {
364
+ dial.number({
365
+ statusCallbackEvent: 'initiated ringing answered completed',
366
+ statusCallback: `${this.BASE_URL}/event/${attrs.callSid}/transfer?${queryUrl}`,
367
+ statusCallbackMethod: 'POST'
368
+ }, settings.transferTo);
369
+ } else {
370
+ dial.sip({}, `sip:${settings.transferTo}${attrs.uriTransferParameters || ''}`);
371
+ }
372
+
373
+ return response.toString();
374
+ }
375
+
376
+ // ==================== Prompt Elements ====================
377
+
378
+ async _addPromptElements(element, msg, attrs, sessionInfo, bargeIn = false) {
379
+ const commands = msg.attributes?.commands || [];
380
+
381
+ for (const command of commands) {
382
+ if (command.type === 'message') {
383
+ await this._addMessageElement(element, command, attrs, sessionInfo, bargeIn);
384
+ } else if (command.type === 'wait' && command.time > 0) {
385
+ element.pause({ length: command.time / 1000 });
386
+ this.lastCallSidVerb[attrs.callSid] = 'pause';
387
+ }
388
+ }
389
+ }
390
+
391
+ async _addMessageElement(element, command, attrs, sessionInfo, bargeIn = false) {
392
+ const { message } = command;
393
+
394
+ if (message.type === 'text') {
395
+ const text = utils.markdownToTwilioSpeech(message.text);
396
+
397
+ if (this.voiceProvider !== VOICE_PROVIDER.TWILIO) {
398
+ const audioUrl = await this._generateTTS(text, attrs, sessionInfo);
399
+ if (audioUrl) {
400
+ const playOptions = {};
401
+ if (bargeIn) {
402
+ playOptions.bargeIn = true;
403
+ }
404
+ element.play(playOptions, audioUrl);
405
+ }
406
+ } else {
407
+ const sayOptions = { voice: attrs.TTS_VOICE_NAME, language: attrs.TTS_VOICE_LANGUAGE };
408
+ if (bargeIn) {
409
+ sayOptions.bargeIn = true;
410
+ }
411
+ element.say(sayOptions, text);
412
+ this.lastCallSidVerb[attrs.callSid] = 'say';
413
+ }
414
+ } else if (message.type === 'frame' && message.metadata?.src) {
415
+
416
+ const playOptions = { loop: 1 };
417
+ if (bargeIn) {
418
+ playOptions.bargeIn = true;
419
+ this.lastCallSidVerb[attrs.callSid] = 'play';
420
+ } else {
421
+ playOptions.loop = 300; // Long audio (if the audio last 1 second, it will play for 5 minutes)
422
+ this.lastCallSidVerb[attrs.callSid] = 'play_loop';
423
+ }
424
+ element.play(playOptions, message.metadata.src);
425
+ }
426
+ }
427
+
428
+ // ==================== TTS Generation ====================
429
+
430
+ async _generateTTS(text, attrs, sessionInfo) {
431
+ try {
432
+ const audioData = await this._callTTSProvider(text, attrs, sessionInfo);
433
+
434
+ if (!audioData) {
435
+ throw new SttError('TTS_FAILED', 'TTS returned no audio data');
436
+ }
437
+
438
+ const fileUrl = await this.uploadService.upload(attrs.callSid, audioData, sessionInfo.user);
439
+ // logger.debug('[TiledeskTwilioTranslator] TTS audio URL:', fileUrl);
440
+ return fileUrl;
441
+ } catch (error) {
442
+ logger.error('[TiledeskTwilioTranslator] TTS generation error:', error);
443
+ return null;
444
+ }
445
+ }
446
+
447
+ async _callTTSProvider(text, attrs, sessionInfo) {
448
+ const providers = {
449
+ [VOICE_PROVIDER.OPENAI]: () => {
450
+ const key = sessionInfo.integrations.find(i => i.type === VOICE_PROVIDER.OPENAI)?.key;
451
+ return this.aiService.textToSpeech(text, attrs.TTS_VOICE_NAME, attrs.TTS_MODEL, key);
452
+ },
453
+ [VOICE_PROVIDER.ELEVENLABS]: () => {
454
+ const key = sessionInfo.integrations.find(i => i.type === VOICE_PROVIDER.ELEVENLABS)?.key;
455
+ return this.aiService.textToSpeechElevenLabs(
456
+ text, attrs.TTS_VOICE_NAME, attrs.TTS_MODEL, attrs.TTS_VOICE_LANGUAGE, key
457
+ );
458
+ }
459
+ };
460
+
461
+ const provider = providers[this.voiceProvider];
462
+ if (!provider) {
463
+ throw new SttError('TTS_FAILED', `Unsupported voice provider: ${this.voiceProvider}`);
464
+ }
465
+
466
+ return provider();
467
+ }
468
+
469
+ // ==================== Helpers ====================
470
+
471
+ _buildIntentQuery(attrs) {
472
+ return `intentName=${querystring.encode(attrs.intentName)}&previousIntentTimestamp=${Date.now()}`;
473
+ }
474
+
475
+ _getNoInputNoMatchActions(msg) {
476
+ const settings = msg.attributes?.commands?.find(cmd => cmd.type === SETTING_MESSAGE)?.settings;
477
+ if (!settings) return null;
478
+
479
+ const { noInputIntent, noMatchIntent } = settings;
480
+ return {
481
+ queryNoInput: noInputIntent ? `button_action=${noInputIntent.substring(1)}` : '',
482
+ queryNoMatch: noMatchIntent ? `button_action=${noMatchIntent.substring(1)}` : ''
483
+ };
484
+ }
485
+
486
+ _getButtonsFromMessage(msg) {
487
+ const command = msg.attributes?.commands?.find(cmd =>
488
+ cmd.type === 'message' && cmd.message?.attributes?.attachment?.buttons
489
+ );
490
+ const buttons = command?.message?.attributes?.attachment?.buttons || [];
491
+ return buttons.filter(b => b.type === 'action');
492
+ }
493
+
494
+ _applyDtmfSettings(msg, gatherOptions, attrs) {
495
+ const settings = msg.attributes?.commands?.find(cmd => cmd.type === SETTING_MESSAGE)?.settings;
496
+ if (!settings) return;
497
+
498
+ if (settings.maxDigits) {
499
+ gatherOptions.numDigits = settings.maxDigits;
500
+ attrs.maxDigits = settings.maxDigits;
501
+ }
502
+ if (settings.terminators) {
503
+ gatherOptions.finishOnKey = settings.terminators;
504
+ attrs.terminators = settings.terminators;
505
+ }
506
+ }
507
+
508
+ // Legacy method for backwards compatibility
509
+ toTiledesk(vxmlMessage) {
510
+ // logger.debug('[TiledeskTwilioTranslator] toTiledesk:', vxmlMessage);
511
+ }
512
+ }
513
+
514
+ module.exports = { TiledeskTwilioTranslator };
@@ -0,0 +1,24 @@
1
+ const axios = require("axios").default;
2
+ const logger = require('./logger');
3
+
4
+ class FileUtils {
5
+
6
+
7
+ async downloadFromUrl(url) {
8
+ try {
9
+ const resbody = await axios({
10
+ url: url,
11
+ responseType: 'arraybuffer',
12
+ method: 'GET'
13
+ });
14
+ const buffer = Buffer.from(resbody.data, 'binary');
15
+ return buffer;
16
+ } catch (err) {
17
+ throw err;
18
+ }
19
+ }
20
+ }
21
+
22
+ var fileUtils = new FileUtils();
23
+
24
+ module.exports = fileUtils;
@@ -0,0 +1,32 @@
1
+ var appRoot = require('app-root-path');
2
+ var winston = require('winston');
3
+ var level = process.env.VOICE_TWILIO_LOG || 'info';
4
+
5
+ var options = {
6
+ file: {
7
+ level:level ,
8
+ filename: `${appRoot}/logs/app.log`,
9
+ handleExceptions: true,
10
+ json: false,
11
+ maxsize: 5242880, // 5MB
12
+ maxFiles: 5,
13
+ colorize: false,
14
+ format: winston.format.simple()
15
+ },
16
+ console: {
17
+ level: level,
18
+ handleExceptions: true,
19
+ json: true,
20
+ colorize: true,
21
+ format: winston.format.simple()
22
+ },
23
+ };
24
+
25
+ var logger = winston.createLogger({
26
+ transports: [
27
+ new winston.transports.Console(options.console)
28
+ ],
29
+ exitOnError: false, // do not exit on handled exceptions
30
+ });
31
+
32
+ module.exports = logger;
@@ -73,27 +73,12 @@ function messageType(msgType, message, user) {
73
73
  }
74
74
 
75
75
  function infoMessageType(msg){
76
- if(msg && msg.attributes.messagelabel && msg.attributes.messagelabel.key === INFO_MESSAGE_TYPE.MEMBER_JOINED_GROUP){
77
- return INFO_MESSAGE_TYPE.MEMBER_JOINED_GROUP
78
- }
79
- if(msg && msg.attributes.messagelabel && msg.attributes.messagelabel.key === INFO_MESSAGE_TYPE.CHAT_CLOSED){
80
- return INFO_MESSAGE_TYPE.CHAT_CLOSED
81
- }
82
- if(msg && msg.attributes.messagelabel && msg.attributes.messagelabel.key === INFO_MESSAGE_TYPE.CHAT_REOPENED){
83
- return INFO_MESSAGE_TYPE.CHAT_REOPENED
84
- }
85
- if(msg && msg.attributes.messagelabel && msg.attributes.messagelabel.key === INFO_MESSAGE_TYPE.TOUCHING_OPERATOR){
86
- return INFO_MESSAGE_TYPE.TOUCHING_OPERATOR
87
- }
88
- if(msg && msg.attributes.messagelabel && msg.attributes.messagelabel.key === INFO_MESSAGE_TYPE.LEAD_UPDATED){
89
- return INFO_MESSAGE_TYPE.LEAD_UPDATED
90
- }
91
- if(msg && msg.attributes.messagelabel && msg.attributes.messagelabel.key === INFO_MESSAGE_TYPE.MEMBER_LEFT_GROUP){
92
- return INFO_MESSAGE_TYPE.MEMBER_LEFT_GROUP
93
- }
94
- if(msg && msg.attributes.messagelabel && msg.attributes.messagelabel.key === INFO_MESSAGE_TYPE.LIVE_PAGE){
95
- return INFO_MESSAGE_TYPE.LIVE_PAGE
96
- }
76
+ const key = msg?.attributes?.messagelabel?.key;
77
+ if (!key) return null;
78
+
79
+ // Return the key directly if it's a valid INFO_MESSAGE_TYPE
80
+ const validTypes = Object.values(INFO_MESSAGE_TYPE);
81
+ return validTypes.includes(key) ? key : null;
97
82
  }
98
83
 
99
84
  module.exports = {isCarousel, isImage, isFile, isAudio, isInfo, isInfoSupport, messageType, infoMessageType}