botium-connector-voip 0.0.20 → 0.0.21
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/src/connector.js
CHANGED
|
@@ -4,7 +4,6 @@ const axios = require('axios')
|
|
|
4
4
|
const http = require('http')
|
|
5
5
|
const https = require('https')
|
|
6
6
|
const debug = require('debug')('botium-connector-voip')
|
|
7
|
-
const path = require('path')
|
|
8
7
|
const mm = require('music-metadata')
|
|
9
8
|
|
|
10
9
|
const Capabilities = {
|
|
@@ -163,12 +162,17 @@ class BotiumConnectorVoip {
|
|
|
163
162
|
const botMsg = {}
|
|
164
163
|
botMsg.messageText = botMsgs.map(m => m.messageText).join(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_DELIMITER] || '')
|
|
165
164
|
botMsg.sourceData = botMsgs.map(m => m.sourceData)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
const firstStart = _.get(botMsgs, '[0].sourceData.data.start', null)
|
|
166
|
+
const lastEnd = _.get(botMsgs, `[${botMsgs.length - 1}].sourceData.data.end`, null)
|
|
167
|
+
const prevEnd = _.get(joinLastPrevMsg, 'sourceData.data.end', null)
|
|
168
|
+
// joinLastPrevMsg can be null (first joined message) OR incomplete (in case JOIN timeout fired before we got any final STT info).
|
|
169
|
+
// In those cases, fall back to "start-of-first-chunk" semantics (same as previous behaviour for nil joinLastPrevMsg).
|
|
170
|
+
if (_.isFinite(firstStart) && _.isFinite(lastEnd)) {
|
|
171
|
+
botMsg.sourceData[0].silenceDuration = (_.isFinite(prevEnd) ? (firstStart - prevEnd) : firstStart)
|
|
172
|
+
botMsg.sourceData[0].voiceDuration = lastEnd - firstStart
|
|
169
173
|
} else {
|
|
170
|
-
botMsg.sourceData[0].silenceDuration =
|
|
171
|
-
botMsg.sourceData[0].voiceDuration =
|
|
174
|
+
botMsg.sourceData[0].silenceDuration = _.isFinite(firstStart) ? firstStart : null
|
|
175
|
+
botMsg.sourceData[0].voiceDuration = (_.isFinite(firstStart) && _.isFinite(lastEnd)) ? (lastEnd - firstStart) : null
|
|
172
176
|
}
|
|
173
177
|
return botMsg
|
|
174
178
|
}
|
|
@@ -199,12 +203,16 @@ class BotiumConnectorVoip {
|
|
|
199
203
|
const connect = async (retryIndex) => {
|
|
200
204
|
retryIndex = retryIndex || 0
|
|
201
205
|
try {
|
|
206
|
+
const workerUrl = this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER]
|
|
207
|
+
? process.env.BOTIUM_VOIP_WORKER_URL
|
|
208
|
+
: this.caps[Capabilities.VOIP_WORKER_URL].replace('wss', 'https').replace('ws', 'http')
|
|
209
|
+
const baseUrl = workerUrl.endsWith('/') ? workerUrl.slice(0, -1) : workerUrl
|
|
202
210
|
const res = await axios({
|
|
203
211
|
method: 'post',
|
|
204
212
|
data: {
|
|
205
213
|
API_KEY: this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_APIKEY : this.caps[Capabilities.VOIP_WORKER_APIKEY]
|
|
206
214
|
},
|
|
207
|
-
url:
|
|
215
|
+
url: `${baseUrl}/initCall`
|
|
208
216
|
})
|
|
209
217
|
if (res) {
|
|
210
218
|
data = res.data
|
|
@@ -356,12 +364,108 @@ class BotiumConnectorVoip {
|
|
|
356
364
|
})
|
|
357
365
|
this.ws.on('message', async (data) => {
|
|
358
366
|
const parsedData = JSON.parse(data)
|
|
367
|
+
// Allow fullRecord delivery even if Stop() was already called.
|
|
368
|
+
// Otherwise the recording can be dropped if the worker streams it late (common on hangup).
|
|
369
|
+
if (this.stopCalled) {
|
|
370
|
+
const allowedTypes = ['fullRecord', 'fullRecordStart', 'fullRecordChunk', 'fullRecordEnd', 'error']
|
|
371
|
+
if (!parsedData || !parsedData.type || !allowedTypes.includes(parsedData.type)) {
|
|
372
|
+
debug(`${this.sessionId} - Stop already called, ignoring incoming message`)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
}
|
|
359
376
|
|
|
360
377
|
const parsedDataLog = _.cloneDeep(parsedData)
|
|
361
|
-
|
|
378
|
+
// Sanitize all potential base64 fields for logging (don't dump huge buffers)
|
|
379
|
+
const sanitizeBase64Fields = (obj, prefix = '') => {
|
|
380
|
+
if (!obj || typeof obj !== 'object') return
|
|
381
|
+
for (const key of Object.keys(obj)) {
|
|
382
|
+
const val = obj[key]
|
|
383
|
+
if (typeof val === 'string' && val.length > 500) {
|
|
384
|
+
obj[key] = `<base64:${val.length}chars>`
|
|
385
|
+
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
386
|
+
sanitizeBase64Fields(val, `${prefix}${key}.`)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
sanitizeBase64Fields(parsedDataLog)
|
|
362
391
|
|
|
363
392
|
debug(JSON.stringify(parsedDataLog, null, 2))
|
|
364
393
|
|
|
394
|
+
const _extractFullRecordBase64 = (pd) => {
|
|
395
|
+
if (!pd) return null
|
|
396
|
+
// Different VOIP workers may put the payload in various fields - search all string fields
|
|
397
|
+
// First try known field names
|
|
398
|
+
const knownFields = [
|
|
399
|
+
pd.fullRecord,
|
|
400
|
+
pd.full_record,
|
|
401
|
+
pd.data?.fullRecord,
|
|
402
|
+
pd.data?.full_record,
|
|
403
|
+
pd.data?.b64_buffer,
|
|
404
|
+
pd.data?.base64,
|
|
405
|
+
pd.data?.full_record_buffer,
|
|
406
|
+
pd.data?.buffer,
|
|
407
|
+
pd.buffer,
|
|
408
|
+
pd.base64,
|
|
409
|
+
pd.audio,
|
|
410
|
+
pd.audioData,
|
|
411
|
+
pd.data?.audio,
|
|
412
|
+
pd.data?.audioData
|
|
413
|
+
]
|
|
414
|
+
for (const val of knownFields) {
|
|
415
|
+
if (typeof val === 'string' && val.length > 100) return val
|
|
416
|
+
}
|
|
417
|
+
// Fallback: find any large string field (likely base64)
|
|
418
|
+
const findLargeString = (obj, depth = 0) => {
|
|
419
|
+
if (!obj || typeof obj !== 'object' || depth > 3) return null
|
|
420
|
+
for (const key of Object.keys(obj)) {
|
|
421
|
+
const val = obj[key]
|
|
422
|
+
if (typeof val === 'string' && val.length > 1000) {
|
|
423
|
+
debug(`${this.sessionId} - Found base64 in field '${key}' (len=${val.length})`)
|
|
424
|
+
return val
|
|
425
|
+
}
|
|
426
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
427
|
+
const found = findLargeString(val, depth + 1)
|
|
428
|
+
if (found) return found
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
return findLargeString(pd)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const emitFullRecordAttachment = (base64) => {
|
|
437
|
+
if (!base64 || typeof base64 !== 'string' || base64.length === 0) {
|
|
438
|
+
// Log available fields (length only) to help diagnose worker payload shape without dumping base64
|
|
439
|
+
try {
|
|
440
|
+
const candidates = {
|
|
441
|
+
fullRecord: typeof parsedData?.fullRecord === 'string' ? parsedData.fullRecord.length : null,
|
|
442
|
+
full_record: typeof parsedData?.full_record === 'string' ? parsedData.full_record.length : null,
|
|
443
|
+
data_fullRecord: typeof parsedData?.data?.fullRecord === 'string' ? parsedData.data.fullRecord.length : null,
|
|
444
|
+
data_full_record: typeof parsedData?.data?.full_record === 'string' ? parsedData.data.full_record.length : null,
|
|
445
|
+
data_b64_buffer: typeof parsedData?.data?.b64_buffer === 'string' ? parsedData.data.b64_buffer.length : null,
|
|
446
|
+
data_base64: typeof parsedData?.data?.base64 === 'string' ? parsedData.data.base64.length : null,
|
|
447
|
+
data_full_record_buffer: typeof parsedData?.data?.full_record_buffer === 'string' ? parsedData.data.full_record_buffer.length : null,
|
|
448
|
+
data_buffer: typeof parsedData?.data?.buffer === 'string' ? parsedData.data.buffer.length : null
|
|
449
|
+
}
|
|
450
|
+
debug(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit. CandidateLengths=${JSON.stringify(candidates)}`)
|
|
451
|
+
} catch (err) {
|
|
452
|
+
debug(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit (diag failed: ${err.message || err})`)
|
|
453
|
+
}
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
debug(`${this.sessionId} - Emitting MESSAGE_ATTACHMENT full_record.wav (base64Len=${base64.length}, stopCalled=${!!this.stopCalled})`)
|
|
457
|
+
this.eventEmitter.emit('MESSAGE_ATTACHMENT', this.container, {
|
|
458
|
+
name: 'full_record.wav',
|
|
459
|
+
mimeType: 'audio/wav',
|
|
460
|
+
base64,
|
|
461
|
+
// Session context from capabilities for correlation
|
|
462
|
+
sessionContext: {
|
|
463
|
+
testSessionId: this.caps.VOIP_TEST_SESSION_ID || null,
|
|
464
|
+
testSessionJobId: this.caps.VOIP_TEST_SESSION_JOB_ID || null
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
365
469
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
366
470
|
this.sessionId = parsedData.voipConfig.sessionId
|
|
367
471
|
}
|
|
@@ -403,6 +507,22 @@ class BotiumConnectorVoip {
|
|
|
403
507
|
sendBotMsg(new Error(`Error: ${parsedData.message}`))
|
|
404
508
|
}
|
|
405
509
|
|
|
510
|
+
// Full record streaming support:
|
|
511
|
+
// - some VOIP workers send the recording in chunks and an end marker
|
|
512
|
+
if (parsedData && parsedData.type === 'fullRecordStart') {
|
|
513
|
+
this.fullRecord = ''
|
|
514
|
+
}
|
|
515
|
+
if (parsedData && parsedData.type === 'fullRecordChunk') {
|
|
516
|
+
const chunk = _extractFullRecordBase64(parsedData)
|
|
517
|
+
if (chunk) this.fullRecord = (this.fullRecord || '') + chunk
|
|
518
|
+
}
|
|
519
|
+
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
520
|
+
const tail = _extractFullRecordBase64(parsedData)
|
|
521
|
+
if (tail) this.fullRecord = (this.fullRecord || '') + tail
|
|
522
|
+
emitFullRecordAttachment(this.fullRecord || tail)
|
|
523
|
+
this.end = true
|
|
524
|
+
}
|
|
525
|
+
|
|
406
526
|
if (parsedData && parsedData.type === 'silence') {
|
|
407
527
|
if (_.isNil(this._getIgnoreSilenceDurationAsserterLogicHook(this.convoStep))) {
|
|
408
528
|
if (_.isNil(this._getJoinLogicHook(this.convoStep)) && parsedData.data.silence.length > 0) {
|
|
@@ -413,11 +533,8 @@ class BotiumConnectorVoip {
|
|
|
413
533
|
}
|
|
414
534
|
|
|
415
535
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
mimeType: 'audio/wav',
|
|
419
|
-
base64: parsedData.fullRecord
|
|
420
|
-
})
|
|
536
|
+
// Non-chunked full record
|
|
537
|
+
emitFullRecordAttachment(_extractFullRecordBase64(parsedData))
|
|
421
538
|
this.end = true
|
|
422
539
|
}
|
|
423
540
|
|
|
@@ -432,7 +549,7 @@ class BotiumConnectorVoip {
|
|
|
432
549
|
}
|
|
433
550
|
}
|
|
434
551
|
this.firstSttInfoReceived = true
|
|
435
|
-
if (this.prevData) {
|
|
552
|
+
if (this.prevData && this.prevData.data && !_.isNil(this.prevData.data.end)) {
|
|
436
553
|
if (!_.isNil(parsedData.data.start)) {
|
|
437
554
|
const silenceDuration = parsedData.data.start - this.prevData.data.end
|
|
438
555
|
const joinLogicHook = this._getJoinLogicHook(this.convoStep)
|
|
@@ -483,7 +600,9 @@ class BotiumConnectorVoip {
|
|
|
483
600
|
this.firstMsg = false
|
|
484
601
|
} else {
|
|
485
602
|
const sourceData = parsedData
|
|
486
|
-
|
|
603
|
+
const start = _.get(parsedData, 'data.start', null)
|
|
604
|
+
const prevEnd = _.get(this.prevData, 'data.end', null)
|
|
605
|
+
sourceData.silenceDuration = (_.isFinite(start) && _.isFinite(prevEnd)) ? (start - prevEnd) : (_.isFinite(start) ? start : null)
|
|
487
606
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start
|
|
488
607
|
botMsg = Object.assign({}, botMsg, { sourceData })
|
|
489
608
|
}
|
|
@@ -504,7 +623,9 @@ class BotiumConnectorVoip {
|
|
|
504
623
|
this.firstMsg = false
|
|
505
624
|
} else {
|
|
506
625
|
const sourceData = parsedData
|
|
507
|
-
|
|
626
|
+
const start = _.get(parsedData, 'data.start', null)
|
|
627
|
+
const prevEnd = _.get(this.prevData, 'data.end', null)
|
|
628
|
+
sourceData.silenceDuration = (_.isFinite(start) && _.isFinite(prevEnd)) ? (start - prevEnd) : (_.isFinite(start) ? start : null)
|
|
508
629
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start
|
|
509
630
|
botMsg = Object.assign({}, botMsg, { sourceData })
|
|
510
631
|
}
|
|
@@ -554,8 +675,24 @@ class BotiumConnectorVoip {
|
|
|
554
675
|
|
|
555
676
|
async UserSays (msg) {
|
|
556
677
|
debug('UserSays called')
|
|
557
|
-
|
|
558
|
-
|
|
678
|
+
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
679
|
+
try {
|
|
680
|
+
const safeLog = {
|
|
681
|
+
messageText: msg && msg.messageText ? String(msg.messageText).substring(0, 200) : undefined,
|
|
682
|
+
buttons: msg && Array.isArray(msg.buttons) ? msg.buttons.length : 0,
|
|
683
|
+
hasMedia: !!(msg && msg.media && msg.media.length > 0),
|
|
684
|
+
mediaMimeType: msg && msg.media && msg.media[0] ? msg.media[0].mimeType : undefined,
|
|
685
|
+
mediaSize: msg && msg.media && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined,
|
|
686
|
+
attachments: msg && Array.isArray(msg.attachments) ? msg.attachments.map(a => ({
|
|
687
|
+
name: a && (a.name || a.title),
|
|
688
|
+
mimeType: a && a.mimeType,
|
|
689
|
+
base64Len: a && a.base64 ? a.base64.length : 0
|
|
690
|
+
})) : []
|
|
691
|
+
}
|
|
692
|
+
debug(safeLog)
|
|
693
|
+
} catch (err) {
|
|
694
|
+
debug(`UserSays: failed to build safe log: ${err.message || err}`)
|
|
695
|
+
}
|
|
559
696
|
|
|
560
697
|
if (!msg.attachments) {
|
|
561
698
|
msg.attachments = []
|
|
@@ -670,6 +807,11 @@ class BotiumConnectorVoip {
|
|
|
670
807
|
|
|
671
808
|
async Stop () {
|
|
672
809
|
debug(`${this.sessionId} - Stop called`)
|
|
810
|
+
this.stopCalled = true
|
|
811
|
+
if (this.silenceTimeout) {
|
|
812
|
+
clearTimeout(this.silenceTimeout)
|
|
813
|
+
this.silenceTimeout = null
|
|
814
|
+
}
|
|
673
815
|
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
|
|
674
816
|
const request = JSON.stringify({
|
|
675
817
|
METHOD: 'stopCall',
|
|
@@ -677,9 +819,12 @@ class BotiumConnectorVoip {
|
|
|
677
819
|
})
|
|
678
820
|
this.ws.send(request)
|
|
679
821
|
await new Promise(resolve => {
|
|
680
|
-
setTimeout(resolve, 100000)
|
|
681
|
-
setInterval(() => {
|
|
822
|
+
const stopTimeout = setTimeout(resolve, 100000)
|
|
823
|
+
this._stopWaitInterval = setInterval(() => {
|
|
682
824
|
if (this.end) {
|
|
825
|
+
clearTimeout(stopTimeout)
|
|
826
|
+
clearInterval(this._stopWaitInterval)
|
|
827
|
+
this._stopWaitInterval = null
|
|
683
828
|
this.wsOpened = false
|
|
684
829
|
this.ws = null
|
|
685
830
|
this.end = false
|