@tiledesk/tiledesk-voice-twilio-connector 0.1.27 → 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 -1529
- 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 -202
- 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,31 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
let lastRequestTime = Date.now();
|
|
4
|
+
|
|
5
|
+
const httpLogger = (req, res, next) => {
|
|
6
|
+
const startTime = Date.now();
|
|
7
|
+
const timeSinceLastRequest = startTime - lastRequestTime;
|
|
8
|
+
lastRequestTime = startTime;
|
|
9
|
+
|
|
10
|
+
// Hook into response finish to calculate processing time
|
|
11
|
+
res.on('finish', () => {
|
|
12
|
+
const duration = Date.now() - startTime;
|
|
13
|
+
const bodySize = req.headers['content-length'] || (req.body ? JSON.stringify(req.body).length : 0);
|
|
14
|
+
|
|
15
|
+
const logData = {
|
|
16
|
+
method: req.method,
|
|
17
|
+
path: req.path,
|
|
18
|
+
query: req.query,
|
|
19
|
+
bodySize: bodySize,
|
|
20
|
+
statusCode: res.statusCode,
|
|
21
|
+
durationMs: duration,
|
|
22
|
+
timeSinceLastRequestMs: timeSinceLastRequest
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
logger.info(`HTTP: ${JSON.stringify(logData)}`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
next();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = httpLogger;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const mongodb = require("mongodb");
|
|
2
|
+
var winston = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
class KVBaseMongo {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Constructor for KVBaseMongo object
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { KVBaseMongo } = require('./KVBaseMongo');
|
|
11
|
+
* let db = new KVBaseMongo("kvstore");
|
|
12
|
+
*
|
|
13
|
+
* @param {KVBASE_COLLECTION} The name of the Mongodb collection used as key-value store. Mandatory.
|
|
14
|
+
*/
|
|
15
|
+
constructor(KVBASE_COLLECTION) {
|
|
16
|
+
if (!KVBASE_COLLECTION) {
|
|
17
|
+
throw new Error('KVBASE_COLLECTION (the name of the Mongodb collection used as key-value store) is mandatory.');
|
|
18
|
+
}
|
|
19
|
+
this.KV_COLLECTION = KVBASE_COLLECTION;
|
|
20
|
+
winston.debug("KV_COLLECTION: " + this.KV_COLLECTION)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async connect(MONGODB_URI) {
|
|
24
|
+
try {
|
|
25
|
+
const client = await mongodb.MongoClient.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
|
|
26
|
+
this.db = client.db();
|
|
27
|
+
await this.db.collection(this.KV_COLLECTION).createIndex(
|
|
28
|
+
{ "key": 1 }, { unique: true }
|
|
29
|
+
);
|
|
30
|
+
//winston.debug("[mongodb] db: ", this.db);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
winston.error(err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async set(k, v) {
|
|
38
|
+
try {
|
|
39
|
+
await this.db.collection(this.KV_COLLECTION).updateOne({key: k}, { $set: { value: v, key: k } }, { upsert: true });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async reuseConnection(db) {
|
|
46
|
+
this.db = db;
|
|
47
|
+
await this.db.collection(this.KV_COLLECTION).createIndex(
|
|
48
|
+
{ "key": 1 }, { unique: true }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async get(k) {
|
|
53
|
+
winston.debug("Searching on Collection " + this.KV_COLLECTION + ' for key: '+ k);
|
|
54
|
+
try {
|
|
55
|
+
const doc = await this.db.collection(this.KV_COLLECTION).findOne({ key: k });
|
|
56
|
+
if (doc) {
|
|
57
|
+
winston.debug("Doc found with key: " + doc.key);
|
|
58
|
+
return doc.value;
|
|
59
|
+
} else {
|
|
60
|
+
winston.debug("No Doc found!");
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
winston.error("Error reading mongodb value", err);
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async remove(k) {
|
|
70
|
+
try {
|
|
71
|
+
await this.db.collection(this.KV_COLLECTION).deleteOne({key: k});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { KVBaseMongo };
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const bodyParser = require("body-parser")
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const winston = require("../utils/logger");
|
|
6
|
+
const pjson = require('../../package.json');
|
|
7
|
+
const fs = require('fs').promises;
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const handlebars = require('handlebars');
|
|
10
|
+
|
|
11
|
+
// tiledesk clients
|
|
12
|
+
const { TiledeskChannel } = require("../services/channels/TiledeskChannel")
|
|
13
|
+
const { TiledeskSubscriptionClient } = require('../services/clients/TiledeskSubscriptionClient');
|
|
14
|
+
|
|
15
|
+
//constant
|
|
16
|
+
const CHANNEL_NAME = require('../utils/constants').CHANNEL_NAME;
|
|
17
|
+
|
|
18
|
+
router.use(bodyParser.json());
|
|
19
|
+
router.use(express.urlencoded({ extended: true }));
|
|
20
|
+
router.use(express.static(path.join(__dirname, '..', 'template')));
|
|
21
|
+
|
|
22
|
+
// Handlebars helpers
|
|
23
|
+
handlebars.registerHelper('isEqual', (a, b) => a == b);
|
|
24
|
+
handlebars.registerHelper('json', (a) => JSON.stringify(a));
|
|
25
|
+
|
|
26
|
+
module.exports = (services) => {
|
|
27
|
+
const { db, redisClient, config } = services;
|
|
28
|
+
const API_URL = config.API_URL;
|
|
29
|
+
const BASE_URL = config.BASE_URL;
|
|
30
|
+
const BRAND_NAME = config.BRAND_NAME || "Tiledesk";
|
|
31
|
+
|
|
32
|
+
// Helper to render template
|
|
33
|
+
async function renderTemplate(templateName, data) {
|
|
34
|
+
const templatePath = path.join(__dirname, '..', 'template', templateName);
|
|
35
|
+
try {
|
|
36
|
+
const html = await fs.readFile(templatePath, 'utf-8');
|
|
37
|
+
const template = handlebars.compile(html);
|
|
38
|
+
return template(data);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
winston.error("Error rendering template:", err);
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
router.get("/", async (req, res) => {
|
|
46
|
+
res.send("Welcome to Tiledesk-VOICE connector (manage ROUTE)");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
router.get('/configure', async (req, res) => {
|
|
50
|
+
winston.debug("(voice) /configure :params", req.query);
|
|
51
|
+
|
|
52
|
+
let project_id = req.query.project_id;
|
|
53
|
+
let token = req.query.token;
|
|
54
|
+
let popup_view = req.query.view === 'popup';
|
|
55
|
+
|
|
56
|
+
if (!project_id || !token) {
|
|
57
|
+
const html = await renderTemplate('error.html', {
|
|
58
|
+
app_version: pjson.version,
|
|
59
|
+
error_message: "Query params project_id and token are required."
|
|
60
|
+
});
|
|
61
|
+
return res.send(html);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
let CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
66
|
+
let settings = await db.get(CONTENT_KEY);
|
|
67
|
+
winston.debug("(voice) settings: ", settings);
|
|
68
|
+
|
|
69
|
+
const tdChannel = new TiledeskChannel({
|
|
70
|
+
API_URL: API_URL,
|
|
71
|
+
redis_client: redisClient
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
let departments = await tdChannel.getDepartments(token, project_id).catch((err) => {
|
|
75
|
+
winston.error("Error getting departments", err.response);
|
|
76
|
+
return [];
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let proxy_url = BASE_URL + "/webhook/" + project_id;
|
|
80
|
+
let status_url = BASE_URL + "/twilio/status";
|
|
81
|
+
|
|
82
|
+
if (!settings) {
|
|
83
|
+
try {
|
|
84
|
+
const subscription = await subscribe(token, project_id, API_URL, BASE_URL, db);
|
|
85
|
+
settings = {
|
|
86
|
+
project_id: project_id,
|
|
87
|
+
token: token,
|
|
88
|
+
subscriptionId: subscription._id,
|
|
89
|
+
secret: subscription.secret
|
|
90
|
+
};
|
|
91
|
+
} catch(e) {
|
|
92
|
+
winston.error("Auto-subscribe failed", e);
|
|
93
|
+
const html = await renderTemplate('error.html', {
|
|
94
|
+
app_version: pjson.version,
|
|
95
|
+
error_message: "Auto-subscribe failed: " + e.message
|
|
96
|
+
});
|
|
97
|
+
return res.send(html);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const html = await renderTemplate('configure.html', {
|
|
102
|
+
app_version: pjson.version,
|
|
103
|
+
project_id: project_id,
|
|
104
|
+
token: token,
|
|
105
|
+
proxy_url: proxy_url,
|
|
106
|
+
status_url: status_url,
|
|
107
|
+
subscription_id: settings.subscriptionId,
|
|
108
|
+
department_id: settings.department_id,
|
|
109
|
+
account_sid: settings.account_sid,
|
|
110
|
+
auth_token: settings.auth_token,
|
|
111
|
+
departments: departments,
|
|
112
|
+
brand_name: BRAND_NAME,
|
|
113
|
+
popup_view: popup_view
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return res.send(html);
|
|
117
|
+
|
|
118
|
+
} catch (err) {
|
|
119
|
+
winston.error("Error in /configure", err);
|
|
120
|
+
const html = await renderTemplate('error.html', {
|
|
121
|
+
app_version: pjson.version,
|
|
122
|
+
error_message: "Internal Server Error: " + err.message
|
|
123
|
+
});
|
|
124
|
+
return res.status(500).send(html);
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
router.post('/update', async (req, res) => {
|
|
129
|
+
winston.debug("(voice) /update", req.body);
|
|
130
|
+
|
|
131
|
+
let project_id = req.body.project_id;
|
|
132
|
+
let token = req.body.token;
|
|
133
|
+
let department_id = req.body.department;
|
|
134
|
+
let account_sid = req.body.account_sid;
|
|
135
|
+
let auth_token = req.body.auth_token;
|
|
136
|
+
|
|
137
|
+
if (!project_id || !token) {
|
|
138
|
+
return res.status(400).send("project_id and token are required");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
142
|
+
let settings = await db.get(CONTENT_KEY);
|
|
143
|
+
|
|
144
|
+
let proxy_url = BASE_URL + "/webhook/" + project_id;
|
|
145
|
+
let status_url = BASE_URL + "/twilio/status";
|
|
146
|
+
|
|
147
|
+
// get departments
|
|
148
|
+
const tdChannel = new TiledeskChannel({
|
|
149
|
+
API_URL: API_URL,
|
|
150
|
+
redis_client: redisClient
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
let departments = await tdChannel.getDepartments(token, project_id).catch((err) => {
|
|
154
|
+
winston.error("Error getting departments", err.response);
|
|
155
|
+
return [];
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
if (settings) {
|
|
160
|
+
settings.department_id = department_id;
|
|
161
|
+
settings.account_sid = account_sid;
|
|
162
|
+
settings.auth_token = auth_token;
|
|
163
|
+
await db.set(CONTENT_KEY, settings);
|
|
164
|
+
} else {
|
|
165
|
+
const subscription = await subscribe(token, project_id, API_URL, BASE_URL, db);
|
|
166
|
+
settings = {
|
|
167
|
+
project_id: project_id,
|
|
168
|
+
token: token,
|
|
169
|
+
subscriptionId: subscription._id,
|
|
170
|
+
secret: subscription.secret,
|
|
171
|
+
department_id: department_id,
|
|
172
|
+
account_sid: account_sid,
|
|
173
|
+
auth_token: auth_token
|
|
174
|
+
};
|
|
175
|
+
await db.set(CONTENT_KEY, settings);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const html = await renderTemplate('configure.html', {
|
|
179
|
+
app_version: pjson.version,
|
|
180
|
+
project_id: project_id,
|
|
181
|
+
token: token,
|
|
182
|
+
proxy_url: proxy_url,
|
|
183
|
+
status_url: status_url,
|
|
184
|
+
show_success_modal: true,
|
|
185
|
+
subscription_id: settings.subscriptionId,
|
|
186
|
+
department_id: settings.department_id,
|
|
187
|
+
account_sid: settings.account_sid,
|
|
188
|
+
auth_token: settings.auth_token,
|
|
189
|
+
departments: departments,
|
|
190
|
+
brand_name: BRAND_NAME
|
|
191
|
+
});
|
|
192
|
+
return res.send(html);
|
|
193
|
+
|
|
194
|
+
} catch (error) {
|
|
195
|
+
winston.error("Update failed", error);
|
|
196
|
+
const html = await renderTemplate('configure.html', {
|
|
197
|
+
app_version: pjson.version,
|
|
198
|
+
project_id: project_id,
|
|
199
|
+
token: token,
|
|
200
|
+
proxy_url: proxy_url,
|
|
201
|
+
status_url: status_url,
|
|
202
|
+
show_error_modal: true,
|
|
203
|
+
departments: departments,
|
|
204
|
+
brand_name: BRAND_NAME
|
|
205
|
+
});
|
|
206
|
+
return res.send(html);
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
router.post('/disconnect', async (req, res) => {
|
|
211
|
+
winston.debug("(voice) /disconnect")
|
|
212
|
+
|
|
213
|
+
let project_id = req.body.project_id;
|
|
214
|
+
let token = req.body.token;
|
|
215
|
+
let subscriptionId = req.body.subscription_id;
|
|
216
|
+
|
|
217
|
+
if (!project_id || !token) {
|
|
218
|
+
return res.status(400).send("project_id and token are required");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
222
|
+
await db.remove(CONTENT_KEY);
|
|
223
|
+
winston.debug("(voice) Content deleted.");
|
|
224
|
+
|
|
225
|
+
let proxy_url = BASE_URL + "/webhook/" + project_id;
|
|
226
|
+
let status_url = BASE_URL + "/twilio/status";
|
|
227
|
+
|
|
228
|
+
// get departments
|
|
229
|
+
const tdChannel = new TiledeskChannel({
|
|
230
|
+
API_URL: API_URL,
|
|
231
|
+
redis_client: redisClient
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
let departments = await tdChannel.getDepartments(token, project_id).catch((err) => {
|
|
235
|
+
winston.error("Error getting departments", err.response);
|
|
236
|
+
return [];
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
if (subscriptionId) {
|
|
241
|
+
await unsubscribe(token, project_id, subscriptionId, API_URL);
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
winston.error("(voice) unsubscribe error: " + err);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const html = await renderTemplate('configure.html', {
|
|
248
|
+
app_version: pjson.version,
|
|
249
|
+
project_id: project_id,
|
|
250
|
+
token: token,
|
|
251
|
+
proxy_url: proxy_url,
|
|
252
|
+
status_url: status_url,
|
|
253
|
+
departments: departments,
|
|
254
|
+
brand_name: BRAND_NAME
|
|
255
|
+
});
|
|
256
|
+
return res.send(html);
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return router;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
async function subscribe(token, project_id, API_URL, BASE_URL, db){
|
|
263
|
+
const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
|
|
264
|
+
|
|
265
|
+
const CONTENT_KEY = CHANNEL_NAME + "-" + project_id;
|
|
266
|
+
const subscription_info = {
|
|
267
|
+
target: BASE_URL + "/tiledesk",
|
|
268
|
+
event: 'message.create.request.channel.'+ CHANNEL_NAME
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const subscription = await tdClient.subscribe(subscription_info);
|
|
273
|
+
winston.debug("(voice) Subscription: ", subscription)
|
|
274
|
+
|
|
275
|
+
let settings = {
|
|
276
|
+
project_id: project_id,
|
|
277
|
+
token: token,
|
|
278
|
+
subscriptionId: subscription._id,
|
|
279
|
+
secret: subscription.secret
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await db.set(CONTENT_KEY, settings);
|
|
283
|
+
return subscription;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function unsubscribe(token, project_id, subscriptionId, API_URL){
|
|
290
|
+
const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
|
|
291
|
+
try {
|
|
292
|
+
const data = await tdClient.unsubscribe(subscriptionId);
|
|
293
|
+
winston.debug("(voice) Subscription: ", data)
|
|
294
|
+
return data;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const VoiceController = require('../controllers/VoiceController');
|
|
4
|
+
|
|
5
|
+
module.exports = (services) => {
|
|
6
|
+
const voiceController = new VoiceController(services);
|
|
7
|
+
|
|
8
|
+
router.get('/', (req, res) => voiceController.index(req, res));
|
|
9
|
+
router.post('/tiledesk', (req, res) => voiceController.tiledesk(req, res));
|
|
10
|
+
router.post('/webhook/:id_project', (req, res) => voiceController.webhook(req, res));
|
|
11
|
+
router.post('/nextblock/:callSid', (req, res) => voiceController.nextblock(req, res));
|
|
12
|
+
router.post('/speechresult/:callSid', (req, res) => voiceController.speechresult(req, res));
|
|
13
|
+
router.post('/record/action/:callSid', (req, res) => voiceController.recordAction(req, res));
|
|
14
|
+
router.post('/record/callback/:callSid', (req, res) => voiceController.recordCallback(req, res));
|
|
15
|
+
router.post('/menublock/:callSid', (req, res) => voiceController.menublock(req, res));
|
|
16
|
+
router.post('/handle/:callSid/:event', (req, res) => voiceController.handleEvent(req, res));
|
|
17
|
+
router.post('/event/:callSid/:event', (req, res) => voiceController.event(req, res));
|
|
18
|
+
router.post('/twilio/status', (req, res) => voiceController.twilioStatus(req, res));
|
|
19
|
+
router.post('/twilio/fail', (req, res) => voiceController.twilioFail(req, res));
|
|
20
|
+
|
|
21
|
+
return router;
|
|
22
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
var winston = require('../utils/logger');
|
|
2
|
+
const axios = require("axios").default;
|
|
3
|
+
const FormData = require('form-data');
|
|
4
|
+
|
|
5
|
+
/*ERROR HANDLER*/
|
|
6
|
+
const { ServiceError } = require('../utils/errors');
|
|
7
|
+
|
|
8
|
+
/*UTILS*/
|
|
9
|
+
const fileUtils = require('../utils/fileUtils.js')
|
|
10
|
+
|
|
11
|
+
class AiService {
|
|
12
|
+
|
|
13
|
+
constructor(config) {
|
|
14
|
+
|
|
15
|
+
if (!config) {
|
|
16
|
+
throw new Error("[AiService] config is mandatory");
|
|
17
|
+
}
|
|
18
|
+
if (!config.OPENAI_ENDPOINT) {
|
|
19
|
+
throw new Error("[AiService] config.OPENAI_ENDPOINT is mandatory");
|
|
20
|
+
}
|
|
21
|
+
if(!config.ELEVENLABS_ENDPOINT){
|
|
22
|
+
throw new Error("[AiService] config.ELEVENLABS_ENDPOINT is mandatory");
|
|
23
|
+
}
|
|
24
|
+
if (!config.API_URL) {
|
|
25
|
+
throw new Error("[AiService] config.API_URL is mandatory");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
this.OPENAI_ENDPOINT = config.OPENAI_ENDPOINT;
|
|
30
|
+
this.ELEVENLABS_ENDPOINT = config.ELEVENLABS_ENDPOINT;
|
|
31
|
+
this.API_URL = config.API_URL;
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async speechToText(fileUrl, model, GPT_KEY) {
|
|
36
|
+
let start_time = new Date();
|
|
37
|
+
winston.debug("[AiService] speechToText url: "+ fileUrl);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
let file;
|
|
41
|
+
try {
|
|
42
|
+
file = await fileUtils.downloadFromUrl(fileUrl);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
winston.error("[AiService] err while downloadFromUrl: ", err);
|
|
45
|
+
throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!file) {
|
|
49
|
+
winston.debug('[AiService] OPENAI speechToText file NOT EXIST: . . . return');
|
|
50
|
+
throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const formData = new FormData();
|
|
54
|
+
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
55
|
+
formData.append('model', model);
|
|
56
|
+
|
|
57
|
+
const resbody = await axios({
|
|
58
|
+
url: `${this.OPENAI_ENDPOINT}/audio/transcriptions`,
|
|
59
|
+
headers: {
|
|
60
|
+
...formData.getHeaders(),
|
|
61
|
+
"Authorization": "Bearer " + GPT_KEY
|
|
62
|
+
},
|
|
63
|
+
data: formData,
|
|
64
|
+
method: 'POST'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let end_time = new Date();
|
|
68
|
+
winston.verbose(`-----> [AiService] OpenAI speechToText time elapsed: ${end_time - start_time} ms`);
|
|
69
|
+
return resbody.data.text;
|
|
70
|
+
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err instanceof ServiceError) throw err;
|
|
73
|
+
winston.error("[AiService] OpenAI STT error", err.message);
|
|
74
|
+
throw new ServiceError('AISERVICE_FAILED', 'OpenAI STT service failed with err:', err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async textToSpeech(text, name, model, GPT_KEY){
|
|
79
|
+
let start_time = new Date();
|
|
80
|
+
winston.debug('[AiService] textToSpeech text:'+ text)
|
|
81
|
+
|
|
82
|
+
const data = {
|
|
83
|
+
model: model,
|
|
84
|
+
input: text,
|
|
85
|
+
voice: name,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
winston.debug('[AiService] textToSpeech config:', data)
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const response = await axios({
|
|
92
|
+
url: `${this.OPENAI_ENDPOINT}/audio/speech`,
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
"Authorization": "Bearer " + GPT_KEY
|
|
96
|
+
},
|
|
97
|
+
responseType: 'arraybuffer',
|
|
98
|
+
data: data,
|
|
99
|
+
method: "POST",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let end_time = new Date();
|
|
103
|
+
winston.verbose(`-----> [AiService] textToSpeech time elapsed: ${end_time - start_time} ms`);
|
|
104
|
+
return response?.data;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
winston.error("[AiService] textToSpeech error: ", err.response?.data);
|
|
107
|
+
throw new ServiceError('AISERVICE_FAILED', 'OpenAI textToSpeech API failed with err:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async speechToTextElevenLabs(fileUrl, model, language, API_KEY) {
|
|
114
|
+
let start_time = new Date();
|
|
115
|
+
winston.debug("[AiService] ELEVEN Labs speechToText url: "+ fileUrl);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
let file;
|
|
119
|
+
try {
|
|
120
|
+
file = await fileUtils.downloadFromUrl(fileUrl);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
winston.error("[AiService] err: ", err);
|
|
123
|
+
throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!file) {
|
|
127
|
+
winston.debug('[AiService] ELEVEN Labs speechToText file NOT EXIST: . . . return');
|
|
128
|
+
throw new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const formData = new FormData();
|
|
132
|
+
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
133
|
+
formData.append('model_id', "scribe_v1");
|
|
134
|
+
formData.append('language_code', language);
|
|
135
|
+
|
|
136
|
+
const resbody = await axios({
|
|
137
|
+
url: `${this.ELEVENLABS_ENDPOINT}/v1/speech-to-text`,
|
|
138
|
+
headers: {
|
|
139
|
+
...formData.getHeaders(),
|
|
140
|
+
"xi-api-key": API_KEY
|
|
141
|
+
},
|
|
142
|
+
data: formData,
|
|
143
|
+
method: 'POST'
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
let end_time = new Date();
|
|
147
|
+
winston.verbose(`-----> [AiService] ELEVEN Labs speechToText time elapsed: ${end_time - start_time} ms`);
|
|
148
|
+
return resbody.data.text;
|
|
149
|
+
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err instanceof ServiceError) throw err;
|
|
152
|
+
winston.error("[AiService] ElevenLabs STT error", err.message);
|
|
153
|
+
throw new ServiceError('AISERVICE_FAILED', 'ElevenLabs STT service failed with err:', err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async textToSpeechElevenLabs(text, voice_id, model, language_code, API_KEY){
|
|
158
|
+
let start_time = new Date();
|
|
159
|
+
const data = {
|
|
160
|
+
model_id: model,
|
|
161
|
+
text: text,
|
|
162
|
+
language_code: language_code
|
|
163
|
+
};
|
|
164
|
+
winston.debug('[AiService] ELEVEN Labs textToSpeech config:', data);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await axios({
|
|
168
|
+
url: `${this.ELEVENLABS_ENDPOINT}/v1/text-to-speech/${voice_id}?output_format=mp3_44100_128`,
|
|
169
|
+
headers: {
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"xi-api-key": API_KEY
|
|
172
|
+
},
|
|
173
|
+
responseType: 'arraybuffer',
|
|
174
|
+
data: data,
|
|
175
|
+
method: "POST",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
let end_time = new Date();
|
|
179
|
+
winston.verbose(`-----> [AiService] ELEVEN Labs textToSpeech time elapsed: ${end_time - start_time} ms`);
|
|
180
|
+
return response?.data;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
winston.error("[AiService] ELEVEN Labs textToSpeech error: ", err);
|
|
183
|
+
throw new ServiceError('AISERVICE_FAILED', 'ElevenLabs textToSpeech API failed with err:', err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async checkQuoteAvailability(projectId, token) {
|
|
190
|
+
|
|
191
|
+
winston.debug("[AiService] checkQuoteAvailability for project: "+ projectId);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const resbody = await axios({
|
|
195
|
+
url: `${this.API_URL}/${projectId}/quotes/tokens`,
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Type': 'application/json',
|
|
198
|
+
'Authorization': token
|
|
199
|
+
},
|
|
200
|
+
method: 'GET'
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (resbody && resbody.data?.isAvailable === true) {
|
|
204
|
+
return true;
|
|
205
|
+
} else {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
winston.error("[AiService] checkQuoteAvailability error: ", err.response?.data);
|
|
210
|
+
throw new ServiceError('AISERVICE_FAILED', 'checkQuoteAvailability API failed with err:', err);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { AiService };
|