botium-connector-voip 0.0.1

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.
@@ -0,0 +1,341 @@
1
+ const { v4: uuidv4 } = require('uuid')
2
+ const WebSocket = require('ws')
3
+ const _ = require('lodash')
4
+ const axios = require('axios')
5
+ const debug = require('debug')('botium-connector-voip')
6
+
7
+ const Capabilities = {
8
+ VOIP_STT_URL_STREAM: 'VOIP_STT_URL_STREAM',
9
+ VOIP_STT_PARAMS_STREAM: 'VOIP_STT_PARAMS_STREAM',
10
+ VOIP_STT_METHOD_STREAM: 'VOIP_STT_METHOD_STREAM',
11
+ VOIP_STT_BODY_STREAM: 'VOIP_STT_BODY_STREAM',
12
+ VOIP_STT_HEADERS: 'VOIP_STT_HEADERS',
13
+ VOIP_STT_TIMEOUT: 'VOIP_STT_TIMEOUT',
14
+ VOIP_TTS_URL: 'VOIP_TTS_URL',
15
+ VOIP_TTS_PARAMS: 'VOIP_TTS_PARAMS',
16
+ VOIP_TTS_METHOD: 'VOIP_TTS_METHOD',
17
+ VOIP_TTS_BODY: 'VOIP_TTS_BODY',
18
+ VOIP_TTS_HEADERS: 'VOIP_TTS_HEADERS',
19
+ VOIP_TTS_TIMEOUT: 'VOIP_TTS_TIMEOUT',
20
+ VOIP_WORKER_URL: 'VOIP_WORKER_URL',
21
+ VOIP_WORKER_APIKEY: 'VOIP_WORKER_APIKEY',
22
+ VOIP_SIP_POOL_CALLER_ENABLE: 'VOIP_SIP_POOL_CALLER_ENABLE',
23
+ VOIP_SIP_CALLER_REGISTRAR_URI: 'VOIP_SIP_CALLER_REGISTRAR_URI',
24
+ VOIP_SIP_CALLER_ADDRESS: 'VOIP_SIP_CALLER_ADDRESS',
25
+ VOIP_SIP_CALLER_USERNAME: 'VOIP_SIP_CALLER_USERNAME',
26
+ VOIP_SIP_CALLER_PASSWORD: 'VOIP_SIP_CALLER_PASSWORD',
27
+ VOIP_SIP_PROXY: 'VOIP_SIP_PROXY',
28
+ VOIP_SIP_REG_HEADERS: 'VOIP_SIP_REG_HEADERS',
29
+ VOIP_SIP_INVITE_HEADERS: 'VOIP_SIP_INVITE_HEADERS',
30
+ VOIP_SIP_CALLEE_URI: 'VOIP_SIP_CALLEE_URI',
31
+ VOIP_ICE_ENABLE: 'VOIP_ICE_ENABLE',
32
+ VOIP_ICE_STUN_SERVERS: 'VOIP_ICE_STUN_SERVERS',
33
+ VOIP_ICE_TURN_ENABLE: 'VOIP_ICE_TURN_ENABLE',
34
+ VOIP_ICE_TURN_SERVER: 'VOIP_ICE_TURN_SERVER',
35
+ VOIP_ICE_TURN_USER: 'VOIP_ICE_TURN_USER',
36
+ VOIP_ICE_TURN_PASSWORD: 'VOIP_ICE_TURN_PASSWORD',
37
+ VOIP_ICE_TURN_PROTOCOL: 'VOIP_ICE_TURN_PROTOCOL'
38
+ }
39
+
40
+ const Defaults = {
41
+ VOIP_STT_METHOD: 'POST',
42
+ VOIP_STT_TIMEOUT: 10000,
43
+ VOIP_TTS_METHOD: 'GET',
44
+ VOIP_TTS_TIMEOUT: 10000
45
+ }
46
+
47
+ class BotiumConnectorVoip {
48
+ constructor ({ queueBotSays, eventEmitter, caps }) {
49
+ this.queueBotSays = queueBotSays
50
+ this.caps = caps
51
+ this.eventEmitter = eventEmitter
52
+ }
53
+
54
+ async Validate () {
55
+ debug('Validate called')
56
+
57
+ if (this.caps.VOIP_TTS_URL) {
58
+ this.axiosTtsParams = {
59
+ url: this.caps.VOIP_TTS_URL,
60
+ params: this._getParams(Capabilities.VOIP_TTS_PARAMS),
61
+ method: this.caps.VOIP_TTS_METHOD,
62
+ timeout: this.caps.VOIP_TTS_TIMEOUT,
63
+ headers: this._getHeaders(Capabilities.VOIP_TTS_HEADERS)
64
+ }
65
+ try {
66
+ const { data } = await axios({
67
+ ...this.axiosTtsParams,
68
+ url: this._getAxiosUrl(this.caps.VOIP_TTS_URL, '/api/status')
69
+ })
70
+ if (data && data.status === 'OK') {
71
+ debug(`Checking TTS Status response: ${this._getAxiosShortenedOutput(data)}`)
72
+ } else {
73
+ throw new Error(`Checking TTS Status failed, response is: ${this._getAxiosShortenedOutput(data)}`)
74
+ }
75
+ } catch (err) {
76
+ throw new Error(`Checking TTS Status failed - ${this._getAxiosErrOutput(err)}`)
77
+ }
78
+ }
79
+
80
+ this.caps = Object.assign({}, Defaults, this.caps)
81
+ }
82
+
83
+ async Start () {
84
+ debug('Start called')
85
+ debug(this.caps[Capabilities.VOIP_TTS_URL])
86
+
87
+ this.view = {
88
+ container: this,
89
+ context: {},
90
+ msg: {},
91
+ botium: {
92
+ conversationId: uuidv4(),
93
+ stepId: null
94
+ }
95
+ }
96
+
97
+ this.fullRecord = ''
98
+ this.end = false
99
+
100
+ return new Promise((resolve, reject) => {
101
+ this.ws = new WebSocket(this.caps[Capabilities.VOIP_WORKER_URL])
102
+
103
+ if (!_.isArray(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
104
+ if (this.caps[Capabilities.VOIP_ICE_STUN_SERVERS] === '') {
105
+ this.caps[Capabilities.VOIP_ICE_STUN_SERVERS] = []
106
+ } else {
107
+ this.caps[Capabilities.VOIP_ICE_STUN_SERVERS] = this.caps[Capabilities.VOIP_ICE_STUN_SERVERS].split(',')
108
+ }
109
+ }
110
+
111
+ this.wsOpened = false
112
+ this.ws.on('open', () => {
113
+ this.wsOpened = true
114
+ debug(`Websocket connection to ${this.caps[Capabilities.VOIP_WORKER_ENDPOINT]} opened.`)
115
+ const request = {
116
+ METHOD: 'initCall',
117
+ API_KEY: this.caps[Capabilities.VOIP_WORKER_APIKEY],
118
+ SIP_CALLER_AUTO: this.caps[Capabilities.VOIP_SIP_POOL_CALLER_ENABLE],
119
+ SIP_PROXY: this.caps[Capabilities.VOIP_SIP_PROXY],
120
+ SIP_CALLER_URI: this.caps[Capabilities.VOIP_SIP_CALLER_URI],
121
+ SIP_CALLER_USERNAME: this.caps[Capabilities.VOIP_SIP_CALLER_USERNAME],
122
+ SIP_CALLER_PASSWORD: this.caps[Capabilities.VOIP_SIP_CALLER_PASSWORD],
123
+ SIP_CALLER_ADDRESS: this.caps[Capabilities.VOIP_SIP_CALLER_ADDRESS],
124
+ SIP_CALLER_REGISTRAR_URI: this.caps[Capabilities.VOIP_SIP_CALLER_REGISTRAR_URI],
125
+ SIP_CALLEE_URI: this.caps[Capabilities.VOIP_SIP_CALLEE_URI],
126
+ SIP_REG_HEADERS: this.caps[Capabilities.VOIP_SIP_REG_HEADERS],
127
+ SIP_INVITE_HEADERS: this.caps[Capabilities.VOIP_SIP_INVITE_HEADERS],
128
+ ICE_ENABLE: this.caps[Capabilities.VOIP_ICE_ENABLE],
129
+ ICE_STUN_SERVERS: this.caps[Capabilities.VOIP_ICE_STUN_SERVERS],
130
+ ICE_TURN_SERVER: this.caps[Capabilities.VOIP_ICE_TURN_SERVER],
131
+ ICE_TURN_USERNAME: this.caps[Capabilities.VOIP_ICE_TURN_USER],
132
+ ICE_TURN_PASSWORD: this.caps[Capabilities.VOIP_ICE_TURN_PASSWORD],
133
+ ICE_TURN_PROTOCOL: this.caps[Capabilities.VOIP_ICE_TURN_PROTOCOL] || 'TCP',
134
+ STT_CONFIG: {
135
+ stt_url: this.caps[Capabilities.VOIP_STT_URL_STREAM],
136
+ stt_params: this.caps[Capabilities.VOIP_STT_PARAMS_STREAM],
137
+ stt_body: this.caps[Capabilities.VOIP_STT_BODY_STREAM] || null
138
+ },
139
+ TTS_CONFIG: {
140
+ tts_url: this.caps[Capabilities.VOIP_TTS_URL],
141
+ tts_params: this.caps[Capabilities.VOIP_TTS_PARAMS],
142
+ tts_body: this.caps[Capabilities.VOIP_TTS_BODY] || null
143
+ }
144
+ }
145
+ debug(request)
146
+ this.ws.send(JSON.stringify(request))
147
+ })
148
+ this.ws.on('close', () => {
149
+ debug(`Websocket connection to ${this.caps[Capabilities.VOIP_WORKER_URL]} closed.`)
150
+ })
151
+ this.ws.on('error', (err) => {
152
+ debug(err)
153
+ if (!this.wsOpened) {
154
+ reject(new Error(`Websocket connection to ${this.caps[Capabilities.VOIP_WORKER_URL]} error: ${err.message || err}`))
155
+ }
156
+ })
157
+ this.ws.on('message', async (data) => {
158
+ const parsedData = JSON.parse(data)
159
+ const botMsgs = []
160
+
161
+ debug(parsedData)
162
+
163
+ if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
164
+ this.sessionId = parsedData.voipConfig.sessionId
165
+ }
166
+
167
+ if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'forbidden') {
168
+ reject(new Error('Cannot connect to VOIP Worker because of wrong APi key'))
169
+ }
170
+
171
+ if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'connected') {
172
+ resolve()
173
+ }
174
+
175
+ if (parsedData && parsedData.type === 'fullRecord') {
176
+ this.end = true
177
+ this.eventEmitter.emit('MESSAGE_ATTACHMENT', this.container, {
178
+ name: 'full_record.wav',
179
+ mimeType: 'audio/wav',
180
+ base64: parsedData.fullRecord
181
+ })
182
+ }
183
+
184
+ if (parsedData && parsedData.data && parsedData.data.final) {
185
+ const botMsg = { messageText: parsedData.data.message, sourceData: parsedData }
186
+ botMsgs.push(botMsg)
187
+ }
188
+
189
+ botMsgs.forEach(botMsg => setTimeout(() => this.queueBotSays(botMsg), 0))
190
+ })
191
+ })
192
+ }
193
+
194
+ async UserSays (msg) {
195
+ debug('UserSays called')
196
+
197
+ debug(msg)
198
+
199
+ if (!msg.attachments) {
200
+ msg.attachments = []
201
+ }
202
+ setTimeout(async () => {
203
+ if (msg && msg.buttons && msg.buttons.length > 0) {
204
+ const request = JSON.stringify({
205
+ METHOD: 'sendDtmf',
206
+ digits: msg.buttons[0].payload,
207
+ sessionId: this.sessionId
208
+ })
209
+ this.ws.send(request)
210
+ } else if (msg && msg.messageText) {
211
+ if (!this.axiosTtsParams) throw new Error('TTS not configured, only audio input supported')
212
+
213
+ const ttsRequest = {
214
+ ...this.axiosTtsParams,
215
+ params: {
216
+ ...(this.axiosTtsParams.params || {}),
217
+ text: msg.messageText
218
+ },
219
+ data: this._getBody(Capabilities.BSP_TTS_BODY),
220
+ responseType: 'arraybuffer'
221
+ }
222
+ msg.sourceData = ttsRequest
223
+
224
+ let ttsResponse = null
225
+ try {
226
+ ttsResponse = await axios(ttsRequest)
227
+ } catch (err) {
228
+ throw new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`)
229
+ }
230
+ if (Buffer.isBuffer(ttsResponse.data)) {
231
+ const request = JSON.stringify({
232
+ METHOD: 'sendAudio',
233
+ PESQ: false,
234
+ sessionId: this.sessionId,
235
+ b64_buffer: ttsResponse.data.toString('base64')
236
+ })
237
+ msg.attachments.push({
238
+ name: 'tts.wav',
239
+ mimeType: 'audio/wav',
240
+ base64: ttsResponse.data.toString('base64')
241
+ })
242
+ this.ws.send(request)
243
+ } else {
244
+ throw new Error(`TTS failed, response is: ${this._getAxiosShortenedOutput(ttsResponse.data)}`)
245
+ }
246
+ }
247
+ if (msg && msg.media && msg.media.length > 0 && msg.media[0].buffer) {
248
+ msg.userInputs.forEach((userInput, index) => {
249
+ const request = JSON.stringify({
250
+ METHOD: 'sendAudio',
251
+ PESQ: userInput.args.filter(a => a.includes(';PESQ')).length > 0,
252
+ sessionId: this.sessionId,
253
+ b64_buffer: msg.media[index].buffer.toString('base64')
254
+ })
255
+ msg.attachments.push({
256
+ name: msg.media[index].mediaUri,
257
+ mimeType: msg.media[index].mimeType,
258
+ base64: msg.media[index].buffer.toString('base64')
259
+ })
260
+ this.ws.send(request)
261
+ })
262
+ }
263
+ }, 500)
264
+ }
265
+
266
+ async Stop () {
267
+ debug('Stop called')
268
+ const request = JSON.stringify({
269
+ METHOD: 'stopCall',
270
+ sessionId: this.sessionId
271
+ })
272
+ this.ws.send(request)
273
+ await new Promise(resolve => {
274
+ setTimeout(resolve, 50000)
275
+ setInterval(() => {
276
+ if (this.end) {
277
+ resolve()
278
+ }
279
+ }, 1000)
280
+ })
281
+
282
+ if (this.ws) {
283
+ this.ws.close()
284
+ }
285
+ this.wsOpened = false
286
+ this.ws = null
287
+ this.view = {}
288
+ }
289
+
290
+ _getParams (capParams) {
291
+ if (this.caps[capParams]) {
292
+ if (_.isString(this.caps[capParams])) return JSON.parse(this.caps[capParams])
293
+ else return this.caps[capParams]
294
+ }
295
+ return {}
296
+ }
297
+
298
+ _getBody (capBody) {
299
+ if (this.caps[capBody]) {
300
+ if (_.isString(this.caps[capBody])) return JSON.parse(this.caps[capBody])
301
+ else return this.caps[capBody]
302
+ }
303
+ return null
304
+ }
305
+
306
+ _getHeaders (capHeaders) {
307
+ if (this.caps[capHeaders]) {
308
+ if (_.isString(this.caps[capHeaders])) return JSON.parse(this.caps[capHeaders])
309
+ else return this.caps[capHeaders]
310
+ }
311
+ return {}
312
+ }
313
+
314
+ _getAxiosUrl (baseUrl, extUrl) {
315
+ return baseUrl.substr(0, baseUrl.indexOf('/', 8)) + extUrl
316
+ }
317
+
318
+ _getAxiosShortenedOutput (data) {
319
+ if (data) {
320
+ if (_.isBuffer(data)) {
321
+ try {
322
+ data = data.toString()
323
+ } catch (err) {
324
+ }
325
+ }
326
+ return _.truncate(_.isString(data) ? data : JSON.stringify(data), { length: 200 })
327
+ } else {
328
+ return ''
329
+ }
330
+ }
331
+
332
+ _getAxiosErrOutput (err) {
333
+ if (err && err.response) {
334
+ return `Status: ${err.response.status} / Response: ${this._getAxiosShortenedOutput(err.response.data)}`
335
+ } else {
336
+ return err.message
337
+ }
338
+ }
339
+ }
340
+
341
+ module.exports = BotiumConnectorVoip