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
- if (lodash__default["default"].isNil(joinLastPrevMsg)) {
183
- botMsg.sourceData[0].silenceDuration = botMsgs[0].sourceData.data.start;
184
- botMsg.sourceData[0].voiceDuration = botMsgs[botMsgs.length - 1].sourceData.data.end - botMsgs[0].sourceData.data.start;
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 = botMsgs[0].sourceData.data.start - joinLastPrevMsg.sourceData.data.end;
187
- botMsg.sourceData[0].voiceDuration = botMsgs[botMsgs.length - 1].sourceData.data.end - botMsgs[0].sourceData.data.start;
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: new URL(path__default["default"].join(`${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')}`, 'initCall')).toString()
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
- parsedDataLog.fullRecord = '<full_record_buffer>';
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
- this.eventEmitter.emit('MESSAGE_ATTACHMENT', this.container, {
415
- name: 'full_record.wav',
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
- sourceData.silenceDuration = parsedData.data.start - this.prevData.data.end;
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
- sourceData.silenceDuration = parsedData.data.start - this.prevData.data.end;
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
- debug$3(msg);
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;