@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,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoiceController - Refactored Version
|
|
3
|
+
*
|
|
4
|
+
* This controller handles HTTP requests/responses only.
|
|
5
|
+
* Business logic has been extracted to dedicated services:
|
|
6
|
+
* - SessionService: Session lifecycle management
|
|
7
|
+
* - MessageService: Queue polling and message retrieval
|
|
8
|
+
* - SpeechService: STT provider abstraction
|
|
9
|
+
* - TiledeskMessageBuilder: Message object factory
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { TiledeskChannel } = require('../services/channels/TiledeskChannel');
|
|
13
|
+
const { TiledeskTwilioTranslator } = require('../services/translators/TiledeskTwilioTranslator');
|
|
14
|
+
const { SessionService } = require('../services/SessionService');
|
|
15
|
+
const { MessageService } = require('../services/MessageService');
|
|
16
|
+
const { SpeechService } = require('../services/SpeechService');
|
|
17
|
+
const { TwilioService } = require('../services/TwilioService');
|
|
18
|
+
const { TiledeskMessageBuilder } = require('../services/TiledeskMessageBuilder');
|
|
19
|
+
const utilsMessage = require('../utils/utils-message');
|
|
20
|
+
const utils = require('../utils/utils');
|
|
21
|
+
const { TYPE_MESSAGE, CHANNEL_NAME, CALL_STATUS } = require('../utils/constants');
|
|
22
|
+
const logger = require('../utils/logger');
|
|
23
|
+
|
|
24
|
+
class VoiceController {
|
|
25
|
+
constructor(services) {
|
|
26
|
+
this.db = services.db;
|
|
27
|
+
this.config = services.config;
|
|
28
|
+
|
|
29
|
+
// Initialize channels
|
|
30
|
+
this.tdChannel = new TiledeskChannel({
|
|
31
|
+
API_URL: this.config.API_URL,
|
|
32
|
+
redis_client: services.redisClient
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.tdTranslator = new TiledeskTwilioTranslator({
|
|
36
|
+
BASE_URL: this.config.BASE_URL,
|
|
37
|
+
aiService: services.aiService,
|
|
38
|
+
uploadService: services.uploadService
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Initialize extracted services
|
|
42
|
+
this.sessionService = new SessionService({
|
|
43
|
+
voiceChannel: services.voiceChannel,
|
|
44
|
+
integrationService: services.integrationService,
|
|
45
|
+
tdChannel: this.tdChannel,
|
|
46
|
+
config: this.config
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.messageService = new MessageService({
|
|
50
|
+
voiceChannel: services.voiceChannel,
|
|
51
|
+
tdChannel: this.tdChannel,
|
|
52
|
+
config: this.config
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.speechService = new SpeechService({
|
|
56
|
+
aiService: services.aiService,
|
|
57
|
+
voiceChannel: services.voiceChannel
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.twilioService = new TwilioService({
|
|
61
|
+
db: this.db,
|
|
62
|
+
config: this.config
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async index(req, res) {
|
|
67
|
+
res.send("Tiledesk Voice Connector");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async tiledesk(req, res) {
|
|
71
|
+
try {
|
|
72
|
+
const tiledeskMessage = req.body.payload;
|
|
73
|
+
const projectId = tiledeskMessage.id_project;
|
|
74
|
+
|
|
75
|
+
logger.debug(`(voice) Message received from Tiledesk in projectID: ${projectId} ---- text: ${tiledeskMessage.text}`);
|
|
76
|
+
|
|
77
|
+
if (!utilsMessage.messageType(TYPE_MESSAGE.INFO, tiledeskMessage) && !(tiledeskMessage.sender.indexOf(CHANNEL_NAME) > -1)) {
|
|
78
|
+
logger.debug(`> whook SAVE MESSAGE "${tiledeskMessage.text}" TO QUEUE at time ${new Date()}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (await this.tdChannel.addMessageToQueue(tiledeskMessage)) {
|
|
82
|
+
await this._handlePlayRedirect(tiledeskMessage, projectId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
res.send("(voice) Message received from Voice Twilio Proxy");
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error("(voice) Error in tiledesk handler:", error);
|
|
88
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async _handlePlayRedirect(tiledeskMessage, projectId) {
|
|
93
|
+
const contentKey = `${CHANNEL_NAME}-${projectId}`;
|
|
94
|
+
await this.twilioService.handlePlayRedirect(
|
|
95
|
+
tiledeskMessage,
|
|
96
|
+
projectId,
|
|
97
|
+
contentKey,
|
|
98
|
+
this.tdTranslator.lastCallSidVerb
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async webhook(req, res) {
|
|
103
|
+
try {
|
|
104
|
+
const startCall = Date.now();
|
|
105
|
+
logger.debug('(voice) called POST /webhook/:id_project ' + new Date(), req.params);
|
|
106
|
+
|
|
107
|
+
const projectId = req.params.id_project;
|
|
108
|
+
const callSid = req.body.CallSid;
|
|
109
|
+
let { From: from, To: to } = req.body;
|
|
110
|
+
|
|
111
|
+
// Validate inputs
|
|
112
|
+
if ((!from || !to) && from !== "client:Anonymous") {
|
|
113
|
+
return res.status(404).send({ error: "Error: Missing from/to parameters" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
from = utils.getNumber(from);
|
|
117
|
+
to = to ? utils.getNumber(to) : "client:AnonymousReceiver";
|
|
118
|
+
|
|
119
|
+
// Get project settings
|
|
120
|
+
const CONTENT_KEY = `${CHANNEL_NAME}-${projectId}`;
|
|
121
|
+
const settings = await this.db.get(CONTENT_KEY);
|
|
122
|
+
if (!settings) {
|
|
123
|
+
return res.status(404).send({ error: "VOICE Channel not already connected" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sign in user
|
|
127
|
+
const user = await this.tdChannel.signIn(from, settings);
|
|
128
|
+
if (!user) {
|
|
129
|
+
return res.status(401).send({ message: `Cannot sign in with caller phone: ${from}` });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Parallel initialization
|
|
133
|
+
const [conversationId, integrations] = await Promise.all([
|
|
134
|
+
this.tdChannel.generateConversation(from, callSid, projectId),
|
|
135
|
+
this.sessionService.getIntegrationKeys(projectId, settings.token)
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
logger.debug(`(voice) conversation returned: ${conversationId}`);
|
|
139
|
+
|
|
140
|
+
// Create session
|
|
141
|
+
const sessionData = await this.sessionService.createSession({
|
|
142
|
+
callSid,
|
|
143
|
+
from,
|
|
144
|
+
to,
|
|
145
|
+
projectId,
|
|
146
|
+
user,
|
|
147
|
+
conversationId,
|
|
148
|
+
integrations
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Send start message
|
|
152
|
+
const tiledeskMessage = TiledeskMessageBuilder.buildStartMessage(from, settings.department_id, req.body);
|
|
153
|
+
const response = await this.tdChannel.send(tiledeskMessage, user.token, conversationId, projectId);
|
|
154
|
+
if (!response) {
|
|
155
|
+
return res.status(503).send({ message: "Bad response: Quota exceeded" });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Get response from queue
|
|
159
|
+
const message = await this.messageService.getNextMessage(callSid, conversationId, from);
|
|
160
|
+
const { vxmlAttributes } = await this.sessionService.getSessionContext(callSid);
|
|
161
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionData);
|
|
162
|
+
|
|
163
|
+
logger.info(`Time to respond to /webhook/${projectId}: ${Date.now() - startCall}[ms]`);
|
|
164
|
+
|
|
165
|
+
res.set('Content-Type', 'text/xml');
|
|
166
|
+
res.status(200).send(messageToVXML);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error("(voice) Error in webhook handler:", error);
|
|
169
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async nextblock(req, res) {
|
|
174
|
+
try {
|
|
175
|
+
const startCall = Date.now();
|
|
176
|
+
const callSid = req.params.callSid;
|
|
177
|
+
const userText = req.body.SpeechResult || req.body.Digits;
|
|
178
|
+
const bargein = req.query.bargein === 'true' || req.query.bargein === true;
|
|
179
|
+
|
|
180
|
+
logger.verbose(`(voice) called POST /nextblock at ${new Date()} with text: ${userText}, bargein: ${bargein}`);
|
|
181
|
+
|
|
182
|
+
const { vxmlAttributes, sessionInfo } = await this.sessionService.getSessionContext(callSid);
|
|
183
|
+
const { from, conversation_id, project_id, user } = sessionInfo;
|
|
184
|
+
|
|
185
|
+
const message = await this._handleUserInput(userText, callSid, from, conversation_id, project_id, user, bargein);
|
|
186
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
187
|
+
|
|
188
|
+
logger.info(`Time to respond to /nextblock/${callSid}: ${Date.now() - startCall}[ms]`);
|
|
189
|
+
|
|
190
|
+
res.set('Content-Type', 'application/xml');
|
|
191
|
+
res.status(200).send(messageToVXML);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error("(voice) Error in nextblock handler:", error);
|
|
194
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async speechresult(req, res) {
|
|
199
|
+
try {
|
|
200
|
+
const startCall = Date.now();
|
|
201
|
+
const callSid = req.params.callSid;
|
|
202
|
+
const userText = req.body.SpeechResult;
|
|
203
|
+
|
|
204
|
+
logger.verbose(`(voice) called POST /speechresult at ${new Date()} with text: ${userText}`);
|
|
205
|
+
|
|
206
|
+
const { vxmlAttributes, sessionInfo } = await this.sessionService.getSessionContext(callSid);
|
|
207
|
+
const { from, conversation_id, project_id, user } = sessionInfo;
|
|
208
|
+
|
|
209
|
+
const message = await this._handleUserInput(userText, callSid, from, conversation_id, project_id, user);
|
|
210
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
211
|
+
|
|
212
|
+
logger.info(`Time to respond to /speechresult/${callSid}: ${Date.now() - startCall}[ms]`);
|
|
213
|
+
|
|
214
|
+
res.set('Content-Type', 'application/xml');
|
|
215
|
+
res.status(200).send(messageToVXML);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.error("(voice) Error in speechresult handler:", error);
|
|
218
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async _handleUserInput(userText, callSid, from, conversationId, projectId, user, bargein = false) {
|
|
223
|
+
if (!userText) {
|
|
224
|
+
return this.messageService.getNextMessage(callSid, conversationId, from);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// If barge-in is enabled and user has provided input, clear the queue
|
|
228
|
+
// This ensures we get the fresh response after user interruption
|
|
229
|
+
if (bargein) {
|
|
230
|
+
logger.debug(`[VoiceController] Barge-in detected, clearing queue for conversation: ${conversationId}`);
|
|
231
|
+
await this.tdChannel.clearQueue(conversationId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const tiledeskMessage = TiledeskMessageBuilder.buildTextMessage(userText, from);
|
|
235
|
+
await this.tdChannel.send(tiledeskMessage, user.token, conversationId, projectId);
|
|
236
|
+
|
|
237
|
+
return this.messageService.getNextMessage(callSid, conversationId, from);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async recordAction(req, res) {
|
|
241
|
+
try {
|
|
242
|
+
const startCall = Date.now();
|
|
243
|
+
const callSid = req.body.CallSid;
|
|
244
|
+
|
|
245
|
+
logger.verbose(`(voice) called POST record/action/:callSid at time ${new Date()}`);
|
|
246
|
+
|
|
247
|
+
const { vxmlAttributes, sessionInfo } = await this.sessionService.getSessionContext(callSid);
|
|
248
|
+
const { from, conversation_id, project_id } = sessionInfo;
|
|
249
|
+
|
|
250
|
+
const message = await this.messageService.getNextMessage(callSid, conversation_id, from);
|
|
251
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
252
|
+
|
|
253
|
+
logger.info(`Time to respond to /record/action/${callSid}: ${Date.now() - startCall}[ms]`);
|
|
254
|
+
|
|
255
|
+
res.set('Content-Type', 'application/xml');
|
|
256
|
+
res.status(200).send(messageToVXML);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logger.error("(voice) Error in recordAction handler:", error);
|
|
259
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async recordCallback(req, res) {
|
|
264
|
+
try {
|
|
265
|
+
const startCall = Date.now();
|
|
266
|
+
const callSid = req.params.callSid || req.body.CallSid;
|
|
267
|
+
const audioFileUrl = req.body.RecordingUrl;
|
|
268
|
+
const buttonAction = req.query.button_action ? `#${req.query.button_action}` : '';
|
|
269
|
+
const previousIntentName = req.query.intentName || '';
|
|
270
|
+
|
|
271
|
+
logger.verbose(`(voice) called POST record/callback/:callSid at time ${new Date()}`);
|
|
272
|
+
|
|
273
|
+
const { sessionInfo, project_id } = await this.sessionService.getSessionContext(callSid);
|
|
274
|
+
const { from, conversation_id, user } = sessionInfo;
|
|
275
|
+
|
|
276
|
+
// Get settings
|
|
277
|
+
const CONTENT_KEY = `${CHANNEL_NAME}-${project_id}`;
|
|
278
|
+
const settings = await this.db.get(CONTENT_KEY);
|
|
279
|
+
if (!settings) {
|
|
280
|
+
return res.status(404).send({ error: "VOICE Channel not already connected" });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// STT transcription
|
|
284
|
+
let tiledeskMessage = await this.speechService.transcribeAudio(audioFileUrl, callSid, sessionInfo, settings);
|
|
285
|
+
|
|
286
|
+
// Handle empty or no-input cases
|
|
287
|
+
if (!tiledeskMessage || !tiledeskMessage.text) {
|
|
288
|
+
tiledeskMessage = TiledeskMessageBuilder.buildNoInputMessage(from, buttonAction, {
|
|
289
|
+
event: 'no_input',
|
|
290
|
+
lastBlock: previousIntentName,
|
|
291
|
+
lastTimestamp: Date.now()
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
const normalizedText = utils.normalizeSTT(tiledeskMessage.text);
|
|
295
|
+
if (!normalizedText) {
|
|
296
|
+
tiledeskMessage = TiledeskMessageBuilder.buildNoInputMessage(from, buttonAction, {
|
|
297
|
+
event: 'no_input',
|
|
298
|
+
lastBlock: previousIntentName,
|
|
299
|
+
lastTimestamp: Date.now()
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
tiledeskMessage.text = normalizedText;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
307
|
+
|
|
308
|
+
logger.info(`Time to respond to /record/callback/${callSid}: ${Date.now() - startCall}[ms] with text ${tiledeskMessage.text}`);
|
|
309
|
+
|
|
310
|
+
res.status(200).send({ success: true, message: `Message sent to Tiledesk for callSid ${callSid}` });
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error("(voice) Error in recordCallback handler:", error);
|
|
313
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async menublock(req, res) {
|
|
318
|
+
try {
|
|
319
|
+
const startCall = Date.now();
|
|
320
|
+
const callSid = req.params.callSid;
|
|
321
|
+
const buttonsMenu = req.query.menu_options;
|
|
322
|
+
const buttonNoMatch = req.query.button_action;
|
|
323
|
+
const menuChoice = req.body.Digits || '';
|
|
324
|
+
|
|
325
|
+
logger.verbose(`/menublock at: ${new Date()} with text: ${menuChoice}`);
|
|
326
|
+
|
|
327
|
+
// Parse menu selection
|
|
328
|
+
const { messageText, attributes } = this._parseMenuSelection(menuChoice, buttonsMenu, buttonNoMatch);
|
|
329
|
+
|
|
330
|
+
const { vxmlAttributes, sessionInfo } = await this.sessionService.getSessionContext(callSid);
|
|
331
|
+
const { from, conversation_id, project_id, user } = sessionInfo;
|
|
332
|
+
|
|
333
|
+
// Send selection to Tiledesk
|
|
334
|
+
const tiledeskMessage = TiledeskMessageBuilder.buildTextMessage(messageText, from, attributes);
|
|
335
|
+
const response = await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
336
|
+
if (!response) {
|
|
337
|
+
return res.status(503).send({ message: "Bad response: Quota exceeded" });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const message = await this.messageService.getNextMessage(callSid, conversation_id, from);
|
|
341
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
342
|
+
|
|
343
|
+
logger.info(`Time to respond to /menublock/${callSid}: ${Date.now() - startCall}[ms]`);
|
|
344
|
+
|
|
345
|
+
res.set('Content-Type', 'application/xml');
|
|
346
|
+
res.status(200).send(messageToVXML);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
logger.error("(voice) Error in menublock handler:", error);
|
|
349
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_parseMenuSelection(menuChoice, buttonsMenu, buttonNoMatch) {
|
|
354
|
+
if (!buttonsMenu) {
|
|
355
|
+
return { messageText: menuChoice.toString(), attributes: {} };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let button = {};
|
|
359
|
+
buttonsMenu.split(';').some((option) => {
|
|
360
|
+
const [value, action] = option.split(':');
|
|
361
|
+
if (value === menuChoice) {
|
|
362
|
+
button = { value, action: `#${action}` };
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (Object.keys(button).length === 0) {
|
|
368
|
+
button = { value: menuChoice, action: `#${buttonNoMatch}` };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
messageText: button.value.toString(),
|
|
373
|
+
attributes: { action: button.action }
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async handleEvent(req, res) {
|
|
378
|
+
try {
|
|
379
|
+
const event = req.params.event;
|
|
380
|
+
const callSid = req.params.callSid;
|
|
381
|
+
const buttonAction = `#${req.query.button_action}`;
|
|
382
|
+
const previousIntentName = req.query.intentName;
|
|
383
|
+
|
|
384
|
+
logger.debug(`(voice) called POST /handle event: ${event}`);
|
|
385
|
+
|
|
386
|
+
const { vxmlAttributes, sessionInfo } = await this.sessionService.getSessionContext(callSid);
|
|
387
|
+
const { from, conversation_id, project_id, user } = sessionInfo;
|
|
388
|
+
|
|
389
|
+
const tiledeskMessage = TiledeskMessageBuilder.buildEventMessage(event, from, {
|
|
390
|
+
action: buttonAction,
|
|
391
|
+
payload: { event, lastBlock: previousIntentName, lastTimestamp: Date.now() }
|
|
392
|
+
});
|
|
393
|
+
await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
394
|
+
|
|
395
|
+
const message = await this.messageService.generateWaitMessage(callSid, from);
|
|
396
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
397
|
+
|
|
398
|
+
res.set('Content-Type', 'application/xml');
|
|
399
|
+
res.status(200).send(messageToVXML);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
logger.error("(voice) Error in handleEvent handler:", error);
|
|
402
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async event(req, res) {
|
|
407
|
+
try {
|
|
408
|
+
const event = req.params.event;
|
|
409
|
+
const callSid = req.params.callSid;
|
|
410
|
+
const currentIntentName = req.query.intentName;
|
|
411
|
+
const currentIntentTimestamp = req.query.previousIntentTimestamp;
|
|
412
|
+
|
|
413
|
+
logger.debug(`(voice) called POST /event: ${event}`);
|
|
414
|
+
|
|
415
|
+
const { vxmlAttributes, sessionInfo } = await this.sessionService.getSessionContext(callSid);
|
|
416
|
+
const { from, conversation_id, project_id, user } = sessionInfo;
|
|
417
|
+
|
|
418
|
+
if (event === 'transfer') {
|
|
419
|
+
await this._handleTransferEvent(req.body.CallStatus, from, conversation_id, project_id, user, {
|
|
420
|
+
event,
|
|
421
|
+
intentName: currentIntentName,
|
|
422
|
+
timestamp: currentIntentTimestamp,
|
|
423
|
+
buttonSuccess: req.query.button_success,
|
|
424
|
+
buttonFailure: req.query.button_failure
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const message = await this.messageService.generateWaitMessage(callSid, from);
|
|
429
|
+
const messageToVXML = await this.tdTranslator.toVXML(message, callSid, vxmlAttributes, sessionInfo);
|
|
430
|
+
|
|
431
|
+
res.set('Content-Type', 'application/xml');
|
|
432
|
+
res.status(200).send(messageToVXML);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
logger.error("(voice) Error in event handler:", error);
|
|
435
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async _handleTransferEvent(callStatus, from, conversationId, projectId, user, options) {
|
|
440
|
+
const statusActions = {
|
|
441
|
+
[CALL_STATUS.COMPLETED]: `#${options.buttonSuccess}`,
|
|
442
|
+
[CALL_STATUS.FAILED]: `#${options.buttonFailure}`
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const buttonAction = statusActions[callStatus];
|
|
446
|
+
if (!buttonAction) return;
|
|
447
|
+
|
|
448
|
+
const tiledeskMessage = TiledeskMessageBuilder.buildTransferMessage(from, options.event, {
|
|
449
|
+
action: buttonAction,
|
|
450
|
+
intentName: options.intentName,
|
|
451
|
+
timestamp: options.timestamp
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
await this.tdChannel.send(tiledeskMessage, user.token, conversationId, projectId);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async twilioStatus(req, res) {
|
|
458
|
+
try {
|
|
459
|
+
const event = req.body.CallStatus;
|
|
460
|
+
const callSid = req.body.CallSid;
|
|
461
|
+
|
|
462
|
+
logger.debug(`(voice) called POST twilio/status: ${event}`);
|
|
463
|
+
|
|
464
|
+
if (event === CALL_STATUS.COMPLETED) {
|
|
465
|
+
const { sessionInfo, conversation_id } = await this.sessionService.getSessionContext(callSid);
|
|
466
|
+
const { from, project_id, user } = sessionInfo;
|
|
467
|
+
|
|
468
|
+
const tiledeskMessage = TiledeskMessageBuilder.buildCloseMessage(from, event);
|
|
469
|
+
await this.tdChannel.send(tiledeskMessage, user.token, conversation_id, project_id);
|
|
470
|
+
|
|
471
|
+
await this.sessionService.cleanupSession(callSid, conversation_id);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
res.status(200).send();
|
|
475
|
+
} catch (error) {
|
|
476
|
+
logger.error("(voice) Error in twilioStatus handler:", error);
|
|
477
|
+
res.status(500).send({ error: "Internal Server Error" });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async twilioFail(req, res) {
|
|
482
|
+
logger.debug('(voice) called POST twilio/fail', req.body);
|
|
483
|
+
res.set('Content-Type', 'application/xml');
|
|
484
|
+
res.status(200).send('<Response></Response>');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = VoiceController;
|