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.
|
@@ -4,7 +4,6 @@ import axios from 'axios';
|
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import https from 'https';
|
|
6
6
|
import debug$4 from 'debug';
|
|
7
|
-
import path from 'path';
|
|
8
7
|
import musicMetadata from 'music-metadata';
|
|
9
8
|
|
|
10
9
|
const debug$3 = debug$4('botium-connector-voip');
|
|
@@ -164,12 +163,17 @@ class BotiumConnectorVoip {
|
|
|
164
163
|
const botMsg = {};
|
|
165
164
|
botMsg.messageText = botMsgs.map(m => m.messageText).join(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_DELIMITER] || '');
|
|
166
165
|
botMsg.sourceData = botMsgs.map(m => m.sourceData);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
const firstStart = lodash.get(botMsgs, '[0].sourceData.data.start', null);
|
|
167
|
+
const lastEnd = lodash.get(botMsgs, `[${botMsgs.length - 1}].sourceData.data.end`, null);
|
|
168
|
+
const prevEnd = lodash.get(joinLastPrevMsg, 'sourceData.data.end', null);
|
|
169
|
+
// joinLastPrevMsg can be null (first joined message) OR incomplete (in case JOIN timeout fired before we got any final STT info).
|
|
170
|
+
// In those cases, fall back to "start-of-first-chunk" semantics (same as previous behaviour for nil joinLastPrevMsg).
|
|
171
|
+
if (lodash.isFinite(firstStart) && lodash.isFinite(lastEnd)) {
|
|
172
|
+
botMsg.sourceData[0].silenceDuration = lodash.isFinite(prevEnd) ? firstStart - prevEnd : firstStart;
|
|
173
|
+
botMsg.sourceData[0].voiceDuration = lastEnd - firstStart;
|
|
170
174
|
} else {
|
|
171
|
-
botMsg.sourceData[0].silenceDuration =
|
|
172
|
-
botMsg.sourceData[0].voiceDuration =
|
|
175
|
+
botMsg.sourceData[0].silenceDuration = lodash.isFinite(firstStart) ? firstStart : null;
|
|
176
|
+
botMsg.sourceData[0].voiceDuration = lodash.isFinite(firstStart) && lodash.isFinite(lastEnd) ? lastEnd - firstStart : null;
|
|
173
177
|
}
|
|
174
178
|
return botMsg;
|
|
175
179
|
};
|
|
@@ -198,12 +202,14 @@ class BotiumConnectorVoip {
|
|
|
198
202
|
const connect = async retryIndex => {
|
|
199
203
|
retryIndex = retryIndex || 0;
|
|
200
204
|
try {
|
|
205
|
+
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');
|
|
206
|
+
const baseUrl = workerUrl.endsWith('/') ? workerUrl.slice(0, -1) : workerUrl;
|
|
201
207
|
const res = await axios({
|
|
202
208
|
method: 'post',
|
|
203
209
|
data: {
|
|
204
210
|
API_KEY: this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_APIKEY : this.caps[Capabilities.VOIP_WORKER_APIKEY]
|
|
205
211
|
},
|
|
206
|
-
url:
|
|
212
|
+
url: `${baseUrl}/initCall`
|
|
207
213
|
});
|
|
208
214
|
if (res) {
|
|
209
215
|
data = res.data;
|
|
@@ -349,9 +355,88 @@ class BotiumConnectorVoip {
|
|
|
349
355
|
});
|
|
350
356
|
this.ws.on('message', async data => {
|
|
351
357
|
const parsedData = JSON.parse(data);
|
|
358
|
+
// Allow fullRecord delivery even if Stop() was already called.
|
|
359
|
+
// Otherwise the recording can be dropped if the worker streams it late (common on hangup).
|
|
360
|
+
if (this.stopCalled) {
|
|
361
|
+
const allowedTypes = ['fullRecord', 'fullRecordStart', 'fullRecordChunk', 'fullRecordEnd', 'error'];
|
|
362
|
+
if (!parsedData || !parsedData.type || !allowedTypes.includes(parsedData.type)) {
|
|
363
|
+
debug$3(`${this.sessionId} - Stop already called, ignoring incoming message`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
352
367
|
const parsedDataLog = lodash.cloneDeep(parsedData);
|
|
353
|
-
|
|
368
|
+
// Sanitize all potential base64 fields for logging (don't dump huge buffers)
|
|
369
|
+
const sanitizeBase64Fields = (obj, prefix = '') => {
|
|
370
|
+
if (!obj || typeof obj !== 'object') return;
|
|
371
|
+
for (const key of Object.keys(obj)) {
|
|
372
|
+
const val = obj[key];
|
|
373
|
+
if (typeof val === 'string' && val.length > 500) {
|
|
374
|
+
obj[key] = `<base64:${val.length}chars>`;
|
|
375
|
+
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
376
|
+
sanitizeBase64Fields(val, `${prefix}${key}.`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
sanitizeBase64Fields(parsedDataLog);
|
|
354
381
|
debug$3(JSON.stringify(parsedDataLog, null, 2));
|
|
382
|
+
const _extractFullRecordBase64 = pd => {
|
|
383
|
+
if (!pd) return null;
|
|
384
|
+
// Different VOIP workers may put the payload in various fields - search all string fields
|
|
385
|
+
// First try known field names
|
|
386
|
+
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];
|
|
387
|
+
for (const val of knownFields) {
|
|
388
|
+
if (typeof val === 'string' && val.length > 100) return val;
|
|
389
|
+
}
|
|
390
|
+
// Fallback: find any large string field (likely base64)
|
|
391
|
+
const findLargeString = (obj, depth = 0) => {
|
|
392
|
+
if (!obj || typeof obj !== 'object' || depth > 3) return null;
|
|
393
|
+
for (const key of Object.keys(obj)) {
|
|
394
|
+
const val = obj[key];
|
|
395
|
+
if (typeof val === 'string' && val.length > 1000) {
|
|
396
|
+
debug$3(`${this.sessionId} - Found base64 in field '${key}' (len=${val.length})`);
|
|
397
|
+
return val;
|
|
398
|
+
}
|
|
399
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
400
|
+
const found = findLargeString(val, depth + 1);
|
|
401
|
+
if (found) return found;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
};
|
|
406
|
+
return findLargeString(pd);
|
|
407
|
+
};
|
|
408
|
+
const emitFullRecordAttachment = base64 => {
|
|
409
|
+
if (!base64 || typeof base64 !== 'string' || base64.length === 0) {
|
|
410
|
+
// Log available fields (length only) to help diagnose worker payload shape without dumping base64
|
|
411
|
+
try {
|
|
412
|
+
const candidates = {
|
|
413
|
+
fullRecord: typeof parsedData?.fullRecord === 'string' ? parsedData.fullRecord.length : null,
|
|
414
|
+
full_record: typeof parsedData?.full_record === 'string' ? parsedData.full_record.length : null,
|
|
415
|
+
data_fullRecord: typeof parsedData?.data?.fullRecord === 'string' ? parsedData.data.fullRecord.length : null,
|
|
416
|
+
data_full_record: typeof parsedData?.data?.full_record === 'string' ? parsedData.data.full_record.length : null,
|
|
417
|
+
data_b64_buffer: typeof parsedData?.data?.b64_buffer === 'string' ? parsedData.data.b64_buffer.length : null,
|
|
418
|
+
data_base64: typeof parsedData?.data?.base64 === 'string' ? parsedData.data.base64.length : null,
|
|
419
|
+
data_full_record_buffer: typeof parsedData?.data?.full_record_buffer === 'string' ? parsedData.data.full_record_buffer.length : null,
|
|
420
|
+
data_buffer: typeof parsedData?.data?.buffer === 'string' ? parsedData.data.buffer.length : null
|
|
421
|
+
};
|
|
422
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit. CandidateLengths=${JSON.stringify(candidates)}`);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit (diag failed: ${err.message || err})`);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
debug$3(`${this.sessionId} - Emitting MESSAGE_ATTACHMENT full_record.wav (base64Len=${base64.length}, stopCalled=${!!this.stopCalled})`);
|
|
429
|
+
this.eventEmitter.emit('MESSAGE_ATTACHMENT', this.container, {
|
|
430
|
+
name: 'full_record.wav',
|
|
431
|
+
mimeType: 'audio/wav',
|
|
432
|
+
base64,
|
|
433
|
+
// Session context from capabilities for correlation
|
|
434
|
+
sessionContext: {
|
|
435
|
+
testSessionId: this.caps.VOIP_TEST_SESSION_ID || null,
|
|
436
|
+
testSessionJobId: this.caps.VOIP_TEST_SESSION_JOB_ID || null
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
};
|
|
355
440
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
356
441
|
this.sessionId = parsedData.voipConfig.sessionId;
|
|
357
442
|
}
|
|
@@ -387,6 +472,22 @@ class BotiumConnectorVoip {
|
|
|
387
472
|
reject(new Error(`Error: ${parsedData.message}`));
|
|
388
473
|
sendBotMsg(new Error(`Error: ${parsedData.message}`));
|
|
389
474
|
}
|
|
475
|
+
|
|
476
|
+
// Full record streaming support:
|
|
477
|
+
// - some VOIP workers send the recording in chunks and an end marker
|
|
478
|
+
if (parsedData && parsedData.type === 'fullRecordStart') {
|
|
479
|
+
this.fullRecord = '';
|
|
480
|
+
}
|
|
481
|
+
if (parsedData && parsedData.type === 'fullRecordChunk') {
|
|
482
|
+
const chunk = _extractFullRecordBase64(parsedData);
|
|
483
|
+
if (chunk) this.fullRecord = (this.fullRecord || '') + chunk;
|
|
484
|
+
}
|
|
485
|
+
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
486
|
+
const tail = _extractFullRecordBase64(parsedData);
|
|
487
|
+
if (tail) this.fullRecord = (this.fullRecord || '') + tail;
|
|
488
|
+
emitFullRecordAttachment(this.fullRecord || tail);
|
|
489
|
+
this.end = true;
|
|
490
|
+
}
|
|
390
491
|
if (parsedData && parsedData.type === 'silence') {
|
|
391
492
|
if (lodash.isNil(this._getIgnoreSilenceDurationAsserterLogicHook(this.convoStep))) {
|
|
392
493
|
if (lodash.isNil(this._getJoinLogicHook(this.convoStep)) && parsedData.data.silence.length > 0) {
|
|
@@ -396,11 +497,8 @@ class BotiumConnectorVoip {
|
|
|
396
497
|
}
|
|
397
498
|
}
|
|
398
499
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
mimeType: 'audio/wav',
|
|
402
|
-
base64: parsedData.fullRecord
|
|
403
|
-
});
|
|
500
|
+
// Non-chunked full record
|
|
501
|
+
emitFullRecordAttachment(_extractFullRecordBase64(parsedData));
|
|
404
502
|
this.end = true;
|
|
405
503
|
}
|
|
406
504
|
if (parsedData && parsedData.data && parsedData.data.final === false) {
|
|
@@ -414,7 +512,7 @@ class BotiumConnectorVoip {
|
|
|
414
512
|
}
|
|
415
513
|
}
|
|
416
514
|
this.firstSttInfoReceived = true;
|
|
417
|
-
if (this.prevData) {
|
|
515
|
+
if (this.prevData && this.prevData.data && !lodash.isNil(this.prevData.data.end)) {
|
|
418
516
|
if (!lodash.isNil(parsedData.data.start)) {
|
|
419
517
|
const silenceDuration = parsedData.data.start - this.prevData.data.end;
|
|
420
518
|
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
@@ -466,7 +564,9 @@ class BotiumConnectorVoip {
|
|
|
466
564
|
this.firstMsg = false;
|
|
467
565
|
} else {
|
|
468
566
|
const sourceData = parsedData;
|
|
469
|
-
|
|
567
|
+
const start = lodash.get(parsedData, 'data.start', null);
|
|
568
|
+
const prevEnd = lodash.get(this.prevData, 'data.end', null);
|
|
569
|
+
sourceData.silenceDuration = lodash.isFinite(start) && lodash.isFinite(prevEnd) ? start - prevEnd : lodash.isFinite(start) ? start : null;
|
|
470
570
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
471
571
|
botMsg = Object.assign({}, botMsg, {
|
|
472
572
|
sourceData
|
|
@@ -493,7 +593,9 @@ class BotiumConnectorVoip {
|
|
|
493
593
|
this.firstMsg = false;
|
|
494
594
|
} else {
|
|
495
595
|
const sourceData = parsedData;
|
|
496
|
-
|
|
596
|
+
const start = lodash.get(parsedData, 'data.start', null);
|
|
597
|
+
const prevEnd = lodash.get(this.prevData, 'data.end', null);
|
|
598
|
+
sourceData.silenceDuration = lodash.isFinite(start) && lodash.isFinite(prevEnd) ? start - prevEnd : lodash.isFinite(start) ? start : null;
|
|
497
599
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
498
600
|
botMsg = Object.assign({}, botMsg, {
|
|
499
601
|
sourceData
|
|
@@ -547,7 +649,24 @@ class BotiumConnectorVoip {
|
|
|
547
649
|
}
|
|
548
650
|
async UserSays(msg) {
|
|
549
651
|
debug$3('UserSays called');
|
|
550
|
-
|
|
652
|
+
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
653
|
+
try {
|
|
654
|
+
const safeLog = {
|
|
655
|
+
messageText: msg && msg.messageText ? String(msg.messageText).substring(0, 200) : undefined,
|
|
656
|
+
buttons: msg && Array.isArray(msg.buttons) ? msg.buttons.length : 0,
|
|
657
|
+
hasMedia: !!(msg && msg.media && msg.media.length > 0),
|
|
658
|
+
mediaMimeType: msg && msg.media && msg.media[0] ? msg.media[0].mimeType : undefined,
|
|
659
|
+
mediaSize: msg && msg.media && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined,
|
|
660
|
+
attachments: msg && Array.isArray(msg.attachments) ? msg.attachments.map(a => ({
|
|
661
|
+
name: a && (a.name || a.title),
|
|
662
|
+
mimeType: a && a.mimeType,
|
|
663
|
+
base64Len: a && a.base64 ? a.base64.length : 0
|
|
664
|
+
})) : []
|
|
665
|
+
};
|
|
666
|
+
debug$3(safeLog);
|
|
667
|
+
} catch (err) {
|
|
668
|
+
debug$3(`UserSays: failed to build safe log: ${err.message || err}`);
|
|
669
|
+
}
|
|
551
670
|
if (!msg.attachments) {
|
|
552
671
|
msg.attachments = [];
|
|
553
672
|
}
|
|
@@ -658,6 +777,11 @@ class BotiumConnectorVoip {
|
|
|
658
777
|
}
|
|
659
778
|
async Stop() {
|
|
660
779
|
debug$3(`${this.sessionId} - Stop called`);
|
|
780
|
+
this.stopCalled = true;
|
|
781
|
+
if (this.silenceTimeout) {
|
|
782
|
+
clearTimeout(this.silenceTimeout);
|
|
783
|
+
this.silenceTimeout = null;
|
|
784
|
+
}
|
|
661
785
|
if (this.ws && this.ws.readyState !== ws.CLOSED) {
|
|
662
786
|
const request = JSON.stringify({
|
|
663
787
|
METHOD: 'stopCall',
|
|
@@ -665,9 +789,12 @@ class BotiumConnectorVoip {
|
|
|
665
789
|
});
|
|
666
790
|
this.ws.send(request);
|
|
667
791
|
await new Promise(resolve => {
|
|
668
|
-
setTimeout(resolve, 100000);
|
|
669
|
-
setInterval(() => {
|
|
792
|
+
const stopTimeout = setTimeout(resolve, 100000);
|
|
793
|
+
this._stopWaitInterval = setInterval(() => {
|
|
670
794
|
if (this.end) {
|
|
795
|
+
clearTimeout(stopTimeout);
|
|
796
|
+
clearInterval(this._stopWaitInterval);
|
|
797
|
+
this._stopWaitInterval = null;
|
|
671
798
|
this.wsOpened = false;
|
|
672
799
|
this.ws = null;
|
|
673
800
|
this.end = false;
|