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.
|
@@ -8,7 +8,6 @@ var axios = require('axios');
|
|
|
8
8
|
var http = require('http');
|
|
9
9
|
var https = require('https');
|
|
10
10
|
var debug$4 = require('debug');
|
|
11
|
-
var path = require('path');
|
|
12
11
|
var musicMetadata = require('music-metadata');
|
|
13
12
|
|
|
14
13
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
@@ -19,7 +18,6 @@ var axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
|
|
|
19
18
|
var http__default = /*#__PURE__*/_interopDefaultLegacy(http);
|
|
20
19
|
var https__default = /*#__PURE__*/_interopDefaultLegacy(https);
|
|
21
20
|
var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug$4);
|
|
22
|
-
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
23
21
|
var musicMetadata__default = /*#__PURE__*/_interopDefaultLegacy(musicMetadata);
|
|
24
22
|
|
|
25
23
|
const debug$3 = debug__default["default"]('botium-connector-voip');
|
|
@@ -179,12 +177,17 @@ class BotiumConnectorVoip {
|
|
|
179
177
|
const botMsg = {};
|
|
180
178
|
botMsg.messageText = botMsgs.map(m => m.messageText).join(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_DELIMITER] || '');
|
|
181
179
|
botMsg.sourceData = botMsgs.map(m => m.sourceData);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
const firstStart = lodash__default["default"].get(botMsgs, '[0].sourceData.data.start', null);
|
|
181
|
+
const lastEnd = lodash__default["default"].get(botMsgs, `[${botMsgs.length - 1}].sourceData.data.end`, null);
|
|
182
|
+
const prevEnd = lodash__default["default"].get(joinLastPrevMsg, 'sourceData.data.end', null);
|
|
183
|
+
// joinLastPrevMsg can be null (first joined message) OR incomplete (in case JOIN timeout fired before we got any final STT info).
|
|
184
|
+
// In those cases, fall back to "start-of-first-chunk" semantics (same as previous behaviour for nil joinLastPrevMsg).
|
|
185
|
+
if (lodash__default["default"].isFinite(firstStart) && lodash__default["default"].isFinite(lastEnd)) {
|
|
186
|
+
botMsg.sourceData[0].silenceDuration = lodash__default["default"].isFinite(prevEnd) ? firstStart - prevEnd : firstStart;
|
|
187
|
+
botMsg.sourceData[0].voiceDuration = lastEnd - firstStart;
|
|
185
188
|
} else {
|
|
186
|
-
botMsg.sourceData[0].silenceDuration =
|
|
187
|
-
botMsg.sourceData[0].voiceDuration =
|
|
189
|
+
botMsg.sourceData[0].silenceDuration = lodash__default["default"].isFinite(firstStart) ? firstStart : null;
|
|
190
|
+
botMsg.sourceData[0].voiceDuration = lodash__default["default"].isFinite(firstStart) && lodash__default["default"].isFinite(lastEnd) ? lastEnd - firstStart : null;
|
|
188
191
|
}
|
|
189
192
|
return botMsg;
|
|
190
193
|
};
|
|
@@ -213,12 +216,14 @@ class BotiumConnectorVoip {
|
|
|
213
216
|
const connect = async retryIndex => {
|
|
214
217
|
retryIndex = retryIndex || 0;
|
|
215
218
|
try {
|
|
219
|
+
const workerUrl = this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_URL : this.caps[Capabilities.VOIP_WORKER_URL].replace('wss', 'https').replace('ws', 'http');
|
|
220
|
+
const baseUrl = workerUrl.endsWith('/') ? workerUrl.slice(0, -1) : workerUrl;
|
|
216
221
|
const res = await axios__default["default"]({
|
|
217
222
|
method: 'post',
|
|
218
223
|
data: {
|
|
219
224
|
API_KEY: this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_APIKEY : this.caps[Capabilities.VOIP_WORKER_APIKEY]
|
|
220
225
|
},
|
|
221
|
-
url:
|
|
226
|
+
url: `${baseUrl}/initCall`
|
|
222
227
|
});
|
|
223
228
|
if (res) {
|
|
224
229
|
data = res.data;
|
|
@@ -364,9 +369,88 @@ class BotiumConnectorVoip {
|
|
|
364
369
|
});
|
|
365
370
|
this.ws.on('message', async data => {
|
|
366
371
|
const parsedData = JSON.parse(data);
|
|
372
|
+
// Allow fullRecord delivery even if Stop() was already called.
|
|
373
|
+
// Otherwise the recording can be dropped if the worker streams it late (common on hangup).
|
|
374
|
+
if (this.stopCalled) {
|
|
375
|
+
const allowedTypes = ['fullRecord', 'fullRecordStart', 'fullRecordChunk', 'fullRecordEnd', 'error'];
|
|
376
|
+
if (!parsedData || !parsedData.type || !allowedTypes.includes(parsedData.type)) {
|
|
377
|
+
debug$3(`${this.sessionId} - Stop already called, ignoring incoming message`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
367
381
|
const parsedDataLog = lodash__default["default"].cloneDeep(parsedData);
|
|
368
|
-
|
|
382
|
+
// Sanitize all potential base64 fields for logging (don't dump huge buffers)
|
|
383
|
+
const sanitizeBase64Fields = (obj, prefix = '') => {
|
|
384
|
+
if (!obj || typeof obj !== 'object') return;
|
|
385
|
+
for (const key of Object.keys(obj)) {
|
|
386
|
+
const val = obj[key];
|
|
387
|
+
if (typeof val === 'string' && val.length > 500) {
|
|
388
|
+
obj[key] = `<base64:${val.length}chars>`;
|
|
389
|
+
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
390
|
+
sanitizeBase64Fields(val, `${prefix}${key}.`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
sanitizeBase64Fields(parsedDataLog);
|
|
369
395
|
debug$3(JSON.stringify(parsedDataLog, null, 2));
|
|
396
|
+
const _extractFullRecordBase64 = pd => {
|
|
397
|
+
if (!pd) return null;
|
|
398
|
+
// Different VOIP workers may put the payload in various fields - search all string fields
|
|
399
|
+
// First try known field names
|
|
400
|
+
const knownFields = [pd.fullRecord, pd.full_record, pd.data?.fullRecord, pd.data?.full_record, pd.data?.b64_buffer, pd.data?.base64, pd.data?.full_record_buffer, pd.data?.buffer, pd.buffer, pd.base64, pd.audio, pd.audioData, pd.data?.audio, pd.data?.audioData];
|
|
401
|
+
for (const val of knownFields) {
|
|
402
|
+
if (typeof val === 'string' && val.length > 100) return val;
|
|
403
|
+
}
|
|
404
|
+
// Fallback: find any large string field (likely base64)
|
|
405
|
+
const findLargeString = (obj, depth = 0) => {
|
|
406
|
+
if (!obj || typeof obj !== 'object' || depth > 3) return null;
|
|
407
|
+
for (const key of Object.keys(obj)) {
|
|
408
|
+
const val = obj[key];
|
|
409
|
+
if (typeof val === 'string' && val.length > 1000) {
|
|
410
|
+
debug$3(`${this.sessionId} - Found base64 in field '${key}' (len=${val.length})`);
|
|
411
|
+
return val;
|
|
412
|
+
}
|
|
413
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
414
|
+
const found = findLargeString(val, depth + 1);
|
|
415
|
+
if (found) return found;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
};
|
|
420
|
+
return findLargeString(pd);
|
|
421
|
+
};
|
|
422
|
+
const emitFullRecordAttachment = base64 => {
|
|
423
|
+
if (!base64 || typeof base64 !== 'string' || base64.length === 0) {
|
|
424
|
+
// Log available fields (length only) to help diagnose worker payload shape without dumping base64
|
|
425
|
+
try {
|
|
426
|
+
const candidates = {
|
|
427
|
+
fullRecord: typeof parsedData?.fullRecord === 'string' ? parsedData.fullRecord.length : null,
|
|
428
|
+
full_record: typeof parsedData?.full_record === 'string' ? parsedData.full_record.length : null,
|
|
429
|
+
data_fullRecord: typeof parsedData?.data?.fullRecord === 'string' ? parsedData.data.fullRecord.length : null,
|
|
430
|
+
data_full_record: typeof parsedData?.data?.full_record === 'string' ? parsedData.data.full_record.length : null,
|
|
431
|
+
data_b64_buffer: typeof parsedData?.data?.b64_buffer === 'string' ? parsedData.data.b64_buffer.length : null,
|
|
432
|
+
data_base64: typeof parsedData?.data?.base64 === 'string' ? parsedData.data.base64.length : null,
|
|
433
|
+
data_full_record_buffer: typeof parsedData?.data?.full_record_buffer === 'string' ? parsedData.data.full_record_buffer.length : null,
|
|
434
|
+
data_buffer: typeof parsedData?.data?.buffer === 'string' ? parsedData.data.buffer.length : null
|
|
435
|
+
};
|
|
436
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit. CandidateLengths=${JSON.stringify(candidates)}`);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit (diag failed: ${err.message || err})`);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
debug$3(`${this.sessionId} - Emitting MESSAGE_ATTACHMENT full_record.wav (base64Len=${base64.length}, stopCalled=${!!this.stopCalled})`);
|
|
443
|
+
this.eventEmitter.emit('MESSAGE_ATTACHMENT', this.container, {
|
|
444
|
+
name: 'full_record.wav',
|
|
445
|
+
mimeType: 'audio/wav',
|
|
446
|
+
base64,
|
|
447
|
+
// Session context from capabilities for correlation
|
|
448
|
+
sessionContext: {
|
|
449
|
+
testSessionId: this.caps.VOIP_TEST_SESSION_ID || null,
|
|
450
|
+
testSessionJobId: this.caps.VOIP_TEST_SESSION_JOB_ID || null
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
};
|
|
370
454
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
371
455
|
this.sessionId = parsedData.voipConfig.sessionId;
|
|
372
456
|
}
|
|
@@ -402,6 +486,22 @@ class BotiumConnectorVoip {
|
|
|
402
486
|
reject(new Error(`Error: ${parsedData.message}`));
|
|
403
487
|
sendBotMsg(new Error(`Error: ${parsedData.message}`));
|
|
404
488
|
}
|
|
489
|
+
|
|
490
|
+
// Full record streaming support:
|
|
491
|
+
// - some VOIP workers send the recording in chunks and an end marker
|
|
492
|
+
if (parsedData && parsedData.type === 'fullRecordStart') {
|
|
493
|
+
this.fullRecord = '';
|
|
494
|
+
}
|
|
495
|
+
if (parsedData && parsedData.type === 'fullRecordChunk') {
|
|
496
|
+
const chunk = _extractFullRecordBase64(parsedData);
|
|
497
|
+
if (chunk) this.fullRecord = (this.fullRecord || '') + chunk;
|
|
498
|
+
}
|
|
499
|
+
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
500
|
+
const tail = _extractFullRecordBase64(parsedData);
|
|
501
|
+
if (tail) this.fullRecord = (this.fullRecord || '') + tail;
|
|
502
|
+
emitFullRecordAttachment(this.fullRecord || tail);
|
|
503
|
+
this.end = true;
|
|
504
|
+
}
|
|
405
505
|
if (parsedData && parsedData.type === 'silence') {
|
|
406
506
|
if (lodash__default["default"].isNil(this._getIgnoreSilenceDurationAsserterLogicHook(this.convoStep))) {
|
|
407
507
|
if (lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep)) && parsedData.data.silence.length > 0) {
|
|
@@ -411,11 +511,8 @@ class BotiumConnectorVoip {
|
|
|
411
511
|
}
|
|
412
512
|
}
|
|
413
513
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
mimeType: 'audio/wav',
|
|
417
|
-
base64: parsedData.fullRecord
|
|
418
|
-
});
|
|
514
|
+
// Non-chunked full record
|
|
515
|
+
emitFullRecordAttachment(_extractFullRecordBase64(parsedData));
|
|
419
516
|
this.end = true;
|
|
420
517
|
}
|
|
421
518
|
if (parsedData && parsedData.data && parsedData.data.final === false) {
|
|
@@ -429,7 +526,7 @@ class BotiumConnectorVoip {
|
|
|
429
526
|
}
|
|
430
527
|
}
|
|
431
528
|
this.firstSttInfoReceived = true;
|
|
432
|
-
if (this.prevData) {
|
|
529
|
+
if (this.prevData && this.prevData.data && !lodash__default["default"].isNil(this.prevData.data.end)) {
|
|
433
530
|
if (!lodash__default["default"].isNil(parsedData.data.start)) {
|
|
434
531
|
const silenceDuration = parsedData.data.start - this.prevData.data.end;
|
|
435
532
|
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
@@ -481,7 +578,9 @@ class BotiumConnectorVoip {
|
|
|
481
578
|
this.firstMsg = false;
|
|
482
579
|
} else {
|
|
483
580
|
const sourceData = parsedData;
|
|
484
|
-
|
|
581
|
+
const start = lodash__default["default"].get(parsedData, 'data.start', null);
|
|
582
|
+
const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
|
|
583
|
+
sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
|
|
485
584
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
486
585
|
botMsg = Object.assign({}, botMsg, {
|
|
487
586
|
sourceData
|
|
@@ -508,7 +607,9 @@ class BotiumConnectorVoip {
|
|
|
508
607
|
this.firstMsg = false;
|
|
509
608
|
} else {
|
|
510
609
|
const sourceData = parsedData;
|
|
511
|
-
|
|
610
|
+
const start = lodash__default["default"].get(parsedData, 'data.start', null);
|
|
611
|
+
const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
|
|
612
|
+
sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
|
|
512
613
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
513
614
|
botMsg = Object.assign({}, botMsg, {
|
|
514
615
|
sourceData
|
|
@@ -562,7 +663,24 @@ class BotiumConnectorVoip {
|
|
|
562
663
|
}
|
|
563
664
|
async UserSays(msg) {
|
|
564
665
|
debug$3('UserSays called');
|
|
565
|
-
|
|
666
|
+
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
667
|
+
try {
|
|
668
|
+
const safeLog = {
|
|
669
|
+
messageText: msg && msg.messageText ? String(msg.messageText).substring(0, 200) : undefined,
|
|
670
|
+
buttons: msg && Array.isArray(msg.buttons) ? msg.buttons.length : 0,
|
|
671
|
+
hasMedia: !!(msg && msg.media && msg.media.length > 0),
|
|
672
|
+
mediaMimeType: msg && msg.media && msg.media[0] ? msg.media[0].mimeType : undefined,
|
|
673
|
+
mediaSize: msg && msg.media && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined,
|
|
674
|
+
attachments: msg && Array.isArray(msg.attachments) ? msg.attachments.map(a => ({
|
|
675
|
+
name: a && (a.name || a.title),
|
|
676
|
+
mimeType: a && a.mimeType,
|
|
677
|
+
base64Len: a && a.base64 ? a.base64.length : 0
|
|
678
|
+
})) : []
|
|
679
|
+
};
|
|
680
|
+
debug$3(safeLog);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
debug$3(`UserSays: failed to build safe log: ${err.message || err}`);
|
|
683
|
+
}
|
|
566
684
|
if (!msg.attachments) {
|
|
567
685
|
msg.attachments = [];
|
|
568
686
|
}
|
|
@@ -673,6 +791,11 @@ class BotiumConnectorVoip {
|
|
|
673
791
|
}
|
|
674
792
|
async Stop() {
|
|
675
793
|
debug$3(`${this.sessionId} - Stop called`);
|
|
794
|
+
this.stopCalled = true;
|
|
795
|
+
if (this.silenceTimeout) {
|
|
796
|
+
clearTimeout(this.silenceTimeout);
|
|
797
|
+
this.silenceTimeout = null;
|
|
798
|
+
}
|
|
676
799
|
if (this.ws && this.ws.readyState !== ws__default["default"].CLOSED) {
|
|
677
800
|
const request = JSON.stringify({
|
|
678
801
|
METHOD: 'stopCall',
|
|
@@ -680,9 +803,12 @@ class BotiumConnectorVoip {
|
|
|
680
803
|
});
|
|
681
804
|
this.ws.send(request);
|
|
682
805
|
await new Promise(resolve => {
|
|
683
|
-
setTimeout(resolve, 100000);
|
|
684
|
-
setInterval(() => {
|
|
806
|
+
const stopTimeout = setTimeout(resolve, 100000);
|
|
807
|
+
this._stopWaitInterval = setInterval(() => {
|
|
685
808
|
if (this.end) {
|
|
809
|
+
clearTimeout(stopTimeout);
|
|
810
|
+
clearInterval(this._stopWaitInterval);
|
|
811
|
+
this._stopWaitInterval = null;
|
|
686
812
|
this.wsOpened = false;
|
|
687
813
|
this.ws = null;
|
|
688
814
|
this.end = false;
|