Compare commits

..

31 Commits

Author SHA1 Message Date
Dave Horton
2a7746537d fix to add custom stt endpoint to session speech credentials object 2022-10-06 07:57:14 +01:00
Dave Horton
b2cac47d05 allow azure stt custom speech endpoint id to be passed as property in recognizer 2022-10-05 21:57:14 +01:00
Dave Horton
8a2b2962f8 gather/transcribe: support for azure custom speech models (endpoint id) 2022-10-04 22:17:28 +01:00
Dave Horton
90cb5e1348 bugfix: typo in bugname was causing transcripts to be ignored 2022-10-04 12:59:58 +01:00
Dave Horton
cf821569b3 minor logging changes 2022-10-02 22:36:27 +01:00
Dave Horton
218f2d6c67 bugfix: unnecessary call to stopTranscription in gather verb when only collecting digits 2022-09-30 10:27:33 +01:00
Joan
c2c8f00978 added call_termination_by on app call status (#168)
Co-authored-by: Joan Salvatella <joan@bookline.io>
2022-09-23 09:13:55 +02:00
Dave Horton
32714d73f3 update to synthAudio with bugfix for writing TTS rtt stats for microsoft 2022-09-21 15:22:19 +02:00
Dave Horton
8da85ebd5a include custom header X-Application-Sid to make it available to cdrs 2022-09-20 13:54:54 +02:00
Dave Horton
dcedf68264 regression bug with adding amd 2022-09-20 09:32:02 +02:00
Dave Horton
05c5d2211f regression bug with parse-url update 2022-09-20 09:31:44 +02:00
Dave Horton
0c089e2380 bugfix: config was not properly enabling amd when configured 2022-09-19 21:16:19 +02:00
Dave Horton
099f33857c update time-series and parse-url 2022-09-16 13:07:08 +02:00
Dave Horton
bd49dacac4 Say length text (#165)
* typo for media bug name in azure and punctuation fix

* say: split very long text intelligently

* more fixes from testing

* update to latest synthAudio
2022-09-14 17:17:29 +02:00
Dave Horton
876824abde typo for media bug name in azure and punctuation fix 2022-09-13 16:22:46 +02:00
Dave Horton
468a9e6d6b make maxPayload of websocket configurable via JAMBONES_WS_MAX_PAYLOAD 2022-09-13 12:35:31 +02:00
Dave Horton
c88163fe11 Bugfix/config stt punctuation (#164)
* support recognizer.punctuation in config verb (#163)

* fixes from testing
2022-09-13 11:45:36 +02:00
xquanluu
bf7ece8f17 feat: play verb support seekOffset and actionHook (#160)
* feat: play verb support seekOffset and actionHook

* add testcase

* fix: testcase
2022-09-13 08:46:16 +02:00
Paulo Telles
e90ef6bc70 change node image and moment package version (#161)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-09-07 13:20:39 +02:00
Dave Horton
a59f6097d7 bump version to 0.7.6 2022-08-26 20:06:04 +02:00
Dave Horton
887c6243e2 handle altLanguages set at the session level via config verb; fix azure stt race condition with final transcripts from stopped recognition 2022-08-25 22:43:38 +02:00
xquanluu
127432f2ec feat: play verb url support single or array list url (#158)
* feat: update time-series 0.11.12

* feat: support play verb url in plain text or array

* fix: review comment

Co-authored-by: Dave Horton <daveh@beachdognet.com>
2022-08-25 10:09:48 +02:00
Dave Horton
4f0439dad9 slight delay when releasing media after call answer, to allow A leg ACK transaction to complete on SBC 2022-08-24 14:25:14 +02:00
Dave Horton
9c188736f9 bugfix: enforce min bargein word count even when we get final transcript (#155) 2022-08-23 16:16:02 +02:00
xquanluu
a69dbb3d4f feat: update time-series 0.11.12 (#153) 2022-08-19 16:28:21 +02:00
Dave Horton
b2e21f06a8 update time-series 2022-08-19 10:00:16 +02:00
Dave Horton
a325bb554a bugfix #152: add key to fsUUIDs periodically 2022-08-19 09:38:40 +02:00
Dave Horton
aa5e3d9437 update undici 2022-08-18 23:25:25 +02:00
Dave Horton
6346954e7a session-level speech hints, strip trailing punctuation on continuous asr (#151) 2022-08-18 23:18:24 +02:00
xquanluu
5b6f7dd3ee feat: add alert for jambonz parsing falure (#148)
* feat: add alert for jambonz parsing falure

* fix: review comment

* fix: update time-series version
2022-08-16 12:39:07 +02:00
Dave Horton
7199db5edb minor performance improvements 2022-08-14 18:28:47 +02:00
37 changed files with 933 additions and 301 deletions

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.6.0-alpine as base
FROM --platform=linux/amd64 node:18.8.0-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3

8
app.js
View File

@@ -16,11 +16,9 @@ const api = require('@opentelemetry/api');
srf.locals = {...srf.locals, otel: {tracer, api}};
const PORT = process.env.HTTP_PORT || 3000;
const opts = {
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const logger = require('pino')(opts);
const opts = {level: process.env.JAMBONES_LOGLEVEL || 'info'};
const pino = require('pino');
const logger = pino(opts, pino.destination({sync: false}));
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
const installSrfLocals = require('./lib/utils/install-srf-locals');
installSrfLocals(srf, logger);

View File

@@ -19,16 +19,22 @@ module.exports = function(srf, logger) {
lookupAppByRealm,
lookupAppByTeamsTenant
} = srf.locals.dbHelpers;
const {
writeAlerts,
AlertType
} = srf.locals;
const {lookupAccountDetails} = dbUtils(logger, srf);
function initLocals(req, res, next) {
const callId = req.get('Call-ID');
logger.info({callId}, 'new incoming call');
if (!req.has('X-Account-Sid')) {
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
return res.send(500);
}
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
const account_sid = req.get('X-Account-Sid');
req.locals = {callSid, account_sid};
req.locals = {callSid, account_sid, callId};
if (req.has('X-Application-Sid')) {
const application_sid = req.get('X-Application-Sid');
logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
@@ -41,7 +47,7 @@ module.exports = function(srf, logger) {
}
function createRootSpan(req, res, next) {
const {callSid, account_sid} = req.locals;
const {callId, callSid, account_sid} = req.locals;
const rootSpan = new RootSpan('incoming-call', req);
const traceId = rootSpan.traceId;
@@ -49,7 +55,7 @@ module.exports = function(srf, logger) {
...req.locals,
traceId,
logger: logger.child({
callId: req.get('Call-ID'),
callId,
callSid,
accountSid: account_sid,
callingNumber: req.callingNumber,
@@ -155,8 +161,7 @@ module.exports = function(srf, logger) {
* Given the dialed DID/phone number, retrieve the application to invoke
*/
async function retrieveApplication(req, res, next) {
const logger = req.locals.logger;
const {accountInfo, account_sid, rootSpan} = req.locals;
const {logger, accountInfo, account_sid, rootSpan} = req.locals;
const {span} = rootSpan.startChildSpan('lookupApplication');
try {
let app;
@@ -261,7 +266,7 @@ module.exports = function(srf, logger) {
const {rootSpan, siprec, application:app} = req.locals;
let span;
try {
if (app.tasks) {
if (app.tasks && !process.env.JAMBONES_MYSQL_REFRESH_TTL) {
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
if (0 === app.tasks.length) throw new Error('no application provided');
return next();
@@ -310,6 +315,12 @@ module.exports = function(srf, logger) {
} catch (err) {
span?.setAttributes({webhookStatus: err.statusCode});
span?.end();
writeAlerts({
account_sid: req.locals.account_sid,
target_sid: req.locals.callSid,
alert_type: AlertType.INVALID_APP_PAYLOAD,
message: `${err?.message}`.trim()
}).catch((err) => this.logger.info({err}, 'Error generating alert for parsing application'));
logger.info({err}, `Error retrieving or parsing application: ${err?.message}`);
res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}});
app.requestor.close();

View File

@@ -11,6 +11,7 @@ class CallInfo {
let srf;
this.direction = opts.direction;
this.traceId = opts.traceId;
this.callTerminationBy = undefined;
if (opts.req) {
const u = opts.req.getParsedHeader('from');
const uri = parseUri(u.uri);
@@ -119,7 +120,7 @@ class CallInfo {
applicationSid: this.applicationSid,
fsSipAddress: this.localSipAddress
};
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName', 'callTerminationBy'].forEach((prop) => {
if (this[prop]) obj[prop] = this[prop];
});
if (typeof this.duration === 'number') obj.duration = this.duration;

View File

@@ -261,6 +261,43 @@ class CallSession extends Emitter {
get recordState() { return this._recordState; }
set globalSttHints({hints, hintsBoost}) {
this._globalSttHints = {hints, hintsBoost};
}
get hasGlobalSttHints() {
const {hints = []} = this._globalSttHints || {};
return hints.length > 0;
}
get globalSttHints() {
return this._globalSttHints;
}
set altLanguages(langs) {
this._globalAltLanguages = langs;
}
get hasAltLanguages() {
return Array.isArray(this._globalAltLanguages);
}
get altLanguages() {
return this._globalAltLanguages;
}
set globalSttPunctuation(punctuate) {
this._globalSttPunctuation = punctuate;
}
get globalSttPunctuation() {
return this._globalSttPunctuation;
}
hasGlobalSttPunctuation() {
return this._globalSttPunctuation !== undefined;
}
async notifyRecordOptions(opts) {
const {action} = opts;
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
@@ -512,7 +549,9 @@ class CallSession extends Emitter {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key,
region: credential.region
region: credential.region,
use_custom_stt: credential.use_custom_stt,
custom_stt_endpoint: credential.custom_stt_endpoint
};
}
else if ('wellsaid' === vendor) {
@@ -601,6 +640,7 @@ class CallSession extends Emitter {
this._onTasksDone();
this._clearResources();
if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid);
}
@@ -1099,7 +1139,6 @@ class CallSession extends Emitter {
* @param {Task} task - task to be executed
*/
async _evalEndpointPrecondition(task) {
this.logger.debug('CallSession:_evalEndpointPrecondition');
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
if (this.ep) {
@@ -1152,6 +1191,7 @@ class CallSession extends Emitter {
} catch (err) {
if (err === CALLER_CANCELLED_ERR_MSG) {
this.logger.error(err, 'caller canceled quickly before we could respond, ending call');
this.callInfo.callTerminationBy = 'caller';
this._notifyCallStatusChange({
callStatus: CallStatus.NoAnswer,
sipStatus: 487,
@@ -1266,7 +1306,8 @@ class CallSession extends Emitter {
this.dlg = await this.srf.createUAS(this.req, this.res, {
headers: {
'X-Trace-ID': this.req.locals.traceId,
'X-Call-Sid': this.req.locals.callSid
'X-Call-Sid': this.req.locals.callSid,
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
},
localSdp: this.ep.local.sdp
});
@@ -1458,8 +1499,9 @@ class CallSession extends Emitter {
dlg.connected = false;
dlg.destroy = origDestroy;
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.callInfo.callTerminationBy = 'jambonz';
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('CallSession: call terminated by jambones');
this.logger.debug('CallSession: call terminated by jambonz');
this.rootSpan.setAttributes({'call.termination': 'hangup by jambonz'});
origDestroy().catch((err) => this.logger.info({err}, 'CallSession - error destroying dialog'));
if (this.wakeupResolver) {

View File

@@ -34,6 +34,7 @@ class InboundCallSession extends CallSession {
_onCancel() {
this.rootSpan.setAttributes({'call.termination': 'caller abandoned'});
this.callInfo.callTerminationBy = 'caller';
this._notifyCallStatusChange({
callStatus: CallStatus.NoAnswer,
sipStatus: 487,
@@ -69,6 +70,7 @@ class InboundCallSession extends CallSession {
assert(this.dlg.connectTime);
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.rootSpan.setAttributes({'call.termination': 'hangup by caller'});
this.callInfo.callTerminationBy = 'caller';
this.emit('callStatusChange', {
callStatus: CallStatus.Completed,
duration

View File

@@ -44,6 +44,7 @@ class RestCallSession extends CallSession {
* This is invoked when the called party hangs up, in order to calculate the call duration.
*/
_callerHungup() {
this.callInfo.callTerminationBy = 'caller';
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('RestCallSession: called party hung up');

View File

@@ -36,7 +36,8 @@ class SipRecCallSession extends InboundCallSession {
headers: {
'Content-Type': 'application/sdp',
'X-Trace-ID': this.req.locals.traceId,
'X-Call-Sid': this.req.locals.callSid
'X-Call-Sid': this.req.locals.callSid,
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
},
localSdp: combinedSdp
});

View File

@@ -541,6 +541,9 @@ class Conference extends Task {
}
this.logger.debug(`Conference:_playHook: executing ${tasks.length} tasks`);
/* we might have been killed while off fetching waitHook */
if (this.killed) return [];
if (tasks.length > 0) {
this._playSession = new ConfirmCallSession({
logger: this.logger,

View File

@@ -26,7 +26,7 @@ class TaskConfig extends Task {
});
}
if (this.bargeIn.sticky) this.autoEnable = true;
this.preconditions = (this.bargeIn.enable || this.record?.action) ?
this.preconditions = (this.bargeIn.enable || this.record?.action || this.data.amd) ?
TaskPreconditions.Endpoint :
TaskPreconditions.None;
}
@@ -54,13 +54,20 @@ class TaskConfig extends Task {
return `${this.name}{${phrase.join(',')}`;
}
async exec(cs) {
async exec(cs, {ep} = {}) {
await super.exec(cs);
if (this.data.amd) {
this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs));
try {
this.ep = ep;
this.startAmd(cs, ep, this, this.data.amd);
} catch (err) {
this.logger.info({err}, 'Config:exec - Error calling startAmd');
}
}
if (this.hasSynthesizer) {
@@ -87,6 +94,20 @@ class TaskConfig extends Task {
cs.asrTimeout = this.recognizer.asrTimeout;
cs.asrDtmfTerminationDigit = this.recognizer.asrDtmfTerminationDigit;
}
if (Array.isArray(this.recognizer.hints)) {
const obj = {hints: this.recognizer.hints};
if (typeof this.recognizer.hintsBoost === 'number') {
obj.hintsBoost = this.recognizer.hintsBoost;
}
cs.globalSttHints = obj;
}
if (Array.isArray(this.recognizer.altLanguages)) {
this.logger.info({altLanguages: this.recognizer.altLanguages}, 'Config: updated altLanguages');
cs.altLanguages = this.recognizer.altLanguages;
}
if ('punctuation' in this.recognizer) {
cs.globalSttPunctuation = this.recognizer.punctuation;
}
this.logger.info({
recognizer: this.recognizer,
isContinuousAsr: cs.isContinuousAsr
@@ -119,6 +140,7 @@ class TaskConfig extends Task {
async kill(cs) {
super.kill(cs);
if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
}
_onAmdEvent(cs, evt) {

View File

@@ -641,7 +641,7 @@ class TaskDial extends Task {
}
/* if we can release the media back to the SBC, do so now */
if (this.canReleaseMedia) this._releaseMedia(cs, sd);
if (this.canReleaseMedia) setTimeout(this._releaseMedia.bind(this, cs, sd), 200);
}
_bridgeEarlyMedia(sd) {

View File

@@ -11,6 +11,19 @@ const makeTask = require('./make_task');
const assert = require('assert');
//const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7);
const compileTranscripts = (logger, evt, arr) => {
//logger.debug({arr, evt}, 'compile transcripts');
if (!Array.isArray(arr) || arr.length === 0) return;
let t = '';
for (const a of arr) {
//logger.debug(`adding ${a.alternatives[0].transcript}`);
t += ` ${a.alternatives[0].transcript}`;
}
t += ` ${evt.alternatives[0].transcript}`;
evt.alternatives[0].transcript = t.trim();
//logger.debug(`compiled transcript: ${evt.alternatives[0].transcript}`);
};
class TaskGather extends Task {
constructor(logger, opts, parentTask) {
super(logger, opts);
@@ -69,6 +82,7 @@ class TaskGather extends Task {
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
this.azureSttEndpointId = recognizer.azureSttEndpointId;
}
else {
this.hints = [];
@@ -86,7 +100,7 @@ class TaskGather extends Task {
}
if (!this.sayTask && !this.playTask) this.listenDuringPrompt = false;
/* buffer speech for continueous asr */
/* buffer speech for continuous asr */
this._bufferedTranscripts = [];
this.parentTask = parentTask;
@@ -121,6 +135,21 @@ class TaskGather extends Task {
await super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
if (cs.hasGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints;
this.hints = this.hints.concat(hints);
if (!this.hintsBoost && hintsBoost) this.hintsBoost = hintsBoost;
this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
'Gather:exec - applying global sttHints');
}
if (cs.hasAltLanguages) {
this.altLanguages = this.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'Gather:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation) {
this.punctuation = cs.globalSttPunctuation;
}
if (!this.isContinuousAsr && cs.isContinuousAsr) {
this.isContinuousAsr = true;
this.asrTimeout = cs.asrTimeout * 1000;
@@ -273,7 +302,7 @@ class TaskGather extends Task {
}
if ('google' === this.vendor) {
this.bugname = 'google_trancribe';
this.bugname = 'google_transcribe';
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
@@ -285,14 +314,16 @@ class TaskGather extends Task {
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
else if (this[arr[0]] === false) opts[arr[1]] = false;
});
if (this.hints.length > 1) {
if (this.hints.length > 0) {
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
if (typeof this.hintsBoost === 'number') {
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
}
}
if (this.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
if (this.altLanguages.length > 0) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
else opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
if ('unspecified' !== this.interactionType) {
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
}
@@ -309,7 +340,7 @@ class TaskGather extends Task {
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
}
else if (['aws', 'polly'].includes(this.vendor)) {
this.bugname = 'aws_trancribe';
this.bugname = 'aws_transcribe';
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
if (this.vocabularyFilterName) {
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
@@ -326,19 +357,30 @@ class TaskGather extends Task {
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
}
else if ('microsoft' === this.vendor) {
this.bugname = 'azure_trancribe';
this.bugname = 'azure_transcribe';
if (this.sttCredentials) {
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
'AZURE_REGION': this.sttCredentials.region
'AZURE_SUBSCRIPTION_KEY': api_key,
'AZURE_REGION': region
});
if (this.azureSttEndpointId) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
}
else if (use_custom_stt && custom_stt_endpoint) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
}
}
if (this.hints && this.hints.length > 1) {
if (this.hints && this.hints.length > 0) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
if (this.altLanguages && this.altLanguages.length > 0) {
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
}
else {
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
}
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
if (this.profanityOption && this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
@@ -448,6 +490,7 @@ class TaskGather extends Task {
_onTranscription(cs, ep, evt, fsEvent) {
// make sure this is not a transcript from answering machine detection
const bugname = fsEvent.getHeader('media-bugname');
const finished = fsEvent.getHeader('transcription-session-finished');
if (bugname && this.bugname !== bugname) return;
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
@@ -480,13 +523,35 @@ class TaskGather extends Task {
};
}
}
/* count words for bargein feature */
const words = evt.alternatives[0].transcript.split(' ').length;
const bufferedWords = this._bufferedTranscripts.reduce((count, e) => {
return count + e.alternatives[0].transcript.split(' ').length;
}, 0);
if (evt.is_final) {
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
return this._startTranscribing(ep);
if ('microsoft' === this.vendor && finished === 'true') {
this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding');
}
else {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
this._startTranscribing(ep);
}
return;
}
if (this.isContinuousAsr) {
/* append the transcript and start listening again for asrTimeout */
const t = evt.alternatives[0].transcript;
if (t) {
/* remove trailing punctuation */
if (/[,;:\.!\?]$/.test(t)) {
this.logger.debug('TaskGather:_onTranscription - removing trailing punctuation');
evt.alternatives[0].transcript = t.slice(0, -1);
}
else this.logger.debug({t}, 'TaskGather:_onTranscription - no trailing punctuation');
}
this.logger.info({evt}, 'TaskGather:_onTranscription - got transcript during continous asr');
this._bufferedTranscripts.push(evt);
this._clearTimer();
@@ -497,7 +562,18 @@ class TaskGather extends Task {
this._startAsrTimer();
return this._startTranscribing(ep);
}
else this._resolve('speech', evt);
else {
if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) {
this.logger.debug({evt, words, bufferedWords},
'TaskGather:_onTranscription - final transcript but < min barge words');
this._bufferedTranscripts.push(evt);
this._startTranscribing(ep);
return;
}
else {
this._resolve('speech', evt);
}
}
}
else {
/* google has a measure of stability:
@@ -505,9 +581,7 @@ class TaskGather extends Task {
others do not.
*/
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
if (this.bargein && /* isStableEnough && */
evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) {
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) {
if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
this.emit('vad');
@@ -528,7 +602,7 @@ class TaskGather extends Task {
this._killAudio(cs);
}
if (!this.resolved && !this.killed) {
if (!this.resolved && !this.killed && !this._bufferedTranscripts.length) {
this._startTranscribing(ep);
}
}
@@ -541,10 +615,17 @@ class TaskGather extends Task {
}
}
_onNoSpeechDetected(cs, ep) {
_onNoSpeechDetected(cs, ep, evt, fsEvent) {
if (!this.callSession.callGone && !this.killed) {
this.logger.debug('TaskGather:_onNoSpeechDetected - listen again');
return this._startTranscribing(ep);
const finished = fsEvent.getHeader('transcription-session-finished');
if (this.vendor === 'microsoft' && finished === 'true') {
this.logger.debug('TaskGather:_onNoSpeechDetected for old gather, ignoring');
}
else {
this.logger.debug('TaskGather:_onNoSpeechDetected - listen again');
this._startTranscribing(ep);
}
return;
}
}
@@ -563,9 +644,13 @@ class TaskGather extends Task {
};
this.logger.debug({evt}, 'TaskGather:resolve continuous asr');
}
else if (!this.isContinuousAsr && reason.startsWith('speech') && this._bufferedTranscripts.length) {
compileTranscripts(this.logger, evt, this._bufferedTranscripts);
this.logger.debug({evt}, 'TaskGather:resolve buffered results');
}
this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
if (this.ep && this.ep.connected) {
if (this.needsStt && this.ep && this.ep.connected) {
this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
}

View File

@@ -7,6 +7,8 @@ class TaskPlay extends Task {
this.preconditions = TaskPreconditions.Endpoint;
this.url = this.data.url;
this.seekOffset = this.data.seekOffset || -1;
this.timeoutSecs = this.data.timeoutSecs || -1;
this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true;
}
@@ -24,9 +26,20 @@ class TaskPlay extends Task {
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
if (cs.isInConference) {
const {memberId, confName, confUuid} = cs;
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
if (Array.isArray(this.url)) {
for (const playUrl of this.url) {
await this.playToConfMember(this.ep, memberId, confName, confUuid, playUrl);
}
} else {
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
}
} else {
const file = (this.timeoutSecs >= 0 || this.seekOffset >= 0) ?
{file: this.url, seekOffset: this.seekOffset, timeoutSecs: this.timeoutSecs} : this.url;
const result = await ep.play(file);
await this.performAction(Object.assign(result, {reason: 'playCompleted'}),
!(this.parentTask || cs.isConfirmCallSession));
}
else await ep.play(this.url);
}
} catch (err) {
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);

View File

@@ -1,12 +1,107 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const breakLengthyTextIfNeeded = (logger, text) => {
const chunkSize = 1000;
if (text.length <= chunkSize) return [text];
const result = [];
const isSSML = text.startsWith('<speak>');
let startPos = 0;
let charPos = isSSML ? 7 : 0; // skip <speak>
let tag;
//logger.debug({isSSML}, `breakLengthyTextIfNeeded: handling text of length ${text.length}`);
while (startPos + charPos < text.length) {
if (isSSML && !tag && text[startPos + charPos] === '<') {
const tagStartPos = ++charPos;
while (startPos + charPos < text.length) {
if (text[startPos + charPos] === '>') {
if (text[startPos + charPos - 1] === '\\') tag = null;
else if (!tag) tag = text.substring(startPos + tagStartPos, startPos + charPos - 1);
break;
}
if (!tag) {
const c = text[startPos + charPos];
if (c === ' ') {
tag = text.substring(startPos + tagStartPos, startPos + charPos);
//logger.debug(`breakLengthyTextIfNeeded: enter tag ${tag} (space)`);
break;
}
}
charPos++;
}
if (tag) {
//search for end of tag
//logger.debug(`breakLengthyTextIfNeeded: searching forward for </${tag}>`);
const e1 = text.indexOf(`</${tag}>`, startPos + charPos);
const e2 = text.indexOf('/>', startPos + charPos);
const tagEndPos = e1 === -1 ? e2 : e2 === -1 ? e1 : Math.min(e1, e2);
if (tagEndPos === -1) {
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} not found, exiting`);
} else {
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} found at ${tagEndPos}`);
charPos = tagEndPos + 1;
}
tag = null;
}
continue;
}
if (charPos < chunkSize) {
charPos++;
continue;
}
// start looking for a good break point
let chunkIt = false;
const a = text[startPos + charPos];
const b = text[startPos + charPos + 1];
if (/[\.!\?]/.test(a) && /\s/.test(b)) {
//logger.debug('breakLengthyTextIfNeeded: breaking at sentence end');
chunkIt = true;
}
if (chunkIt) {
charPos++;
const chunk = text.substr(startPos, charPos);
if (isSSML) {
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}</speak>`);
}
else result.push(chunk);
charPos = 0;
startPos += chunk.length;
//logger.debug({chunk: result[result.length - 1]},
// `breakLengthyTextIfNeeded: chunked; new starting pos ${startPos}`);
}
else charPos++;
}
// final chunk
if (startPos < text.length) {
const chunk = text.substr(startPos);
if (isSSML) {
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}`);
}
else result.push(chunk);
//logger.debug({chunk: result[result.length - 1]},
// `breakLengthyTextIfNeeded: final chunk; starting pos ${startPos} length ${chunk.length}`);
}
return result;
};
class TaskSay extends Task {
constructor(logger, opts, parentTask) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
this.text = Array.isArray(this.data.text) ? this.data.text : [this.data.text];
this.text = (Array.isArray(this.data.text) ? this.data.text : [this.data.text])
.map((t) => breakLengthyTextIfNeeded(this.logger, t))
.flat();
this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
this.synthesizer = this.data.synthesizer || {};

View File

@@ -84,7 +84,7 @@
},
"leave": {
"properties": {
}
},
"hangup": {
@@ -96,9 +96,12 @@
},
"play": {
"properties": {
"url": "string",
"url": "string|array",
"loop": "number|string",
"earlyMedia": "boolean"
"earlyMedia": "boolean",
"seekOffset": "number|string",
"timeoutSecs": "number|string",
"actionHook": "object|string"
},
"required": [
"url"
@@ -494,6 +497,7 @@
"requestSnr": "boolean",
"initialSpeechTimeoutMs": "number",
"azureServiceEndpoint": "string",
"azureSttEndpointId": "string",
"asrDtmfTerminationDigit": "string",
"asrTimeout": "number"
},
@@ -514,7 +518,7 @@
"properties": {
"enable": "boolean",
"voiceMs": "number",
"mode": "number"
"mode": "number"
},
"required": [
"enable"
@@ -536,7 +540,7 @@
"noSpeechTimeoutMs": "number",
"decisionTimeoutMs": "number",
"toneTimeoutMs": "number",
"greetingCompletionTimeoutMs": "number"
"greetingCompletionTimeoutMs": "number"
}
}
}

View File

@@ -54,6 +54,7 @@ class TaskTranscribe extends Task {
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
this.azureSttEndpointId = recognizer.azureSttEndpointId;
}
get name() { return TaskName.Transcribe; }
@@ -62,6 +63,22 @@ class TaskTranscribe extends Task {
super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
if (cs.hasGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints;
this.hints = this.hints.concat(hints);
if (!this.hintsBoost && hintsBoost) this.hintsBoost = hintsBoost;
this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
'Transcribe:exec - applying global `sttHints');
}
if (cs.hasAltLanguages) {
this.altLanguages = this.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'Gather:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation) {
this.punctuation = cs.globalSttPunctuation;
}
this.ep = ep;
this.ep2 = ep2;
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
@@ -145,7 +162,7 @@ class TaskTranscribe extends Task {
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
if (this.vendor === 'google') {
this.bugname = 'google_trancribe';
this.bugname = 'google_transcribe';
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
@@ -157,14 +174,16 @@ class TaskTranscribe extends Task {
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
else if (this[arr[0]] === false) opts[arr[1]] = false;
});
if (this.hints.length > 1) {
if (this.hints.length > 0) {
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
if (typeof this.hintsBoost === 'number') {
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
}
}
if (this.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
if (this.altLanguages.length > 0) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
else opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
if ('unspecified' !== this.interactionType) {
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
}
@@ -181,7 +200,7 @@ class TaskTranscribe extends Task {
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
}
else if (this.vendor === 'aws') {
this.bugname = 'aws_trancribe';
this.bugname = 'aws_transcribe';
[
['diarization', 'AWS_SHOW_SPEAKER_LABEL'],
['identifyChannels', 'AWS_ENABLE_CHANNEL_IDENTIFICATION']
@@ -213,15 +232,23 @@ class TaskTranscribe extends Task {
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
}
else if (this.vendor === 'microsoft') {
this.bugname = 'azure_trancribe';
this.bugname = 'azure_transcribe';
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
'AZURE_REGION': this.sttCredentials.region
'AZURE_SUBSCRIPTION_KEY': api_key,
'AZURE_REGION': region
});
if (this.hints && this.hints.length > 1) {
if (this.azureSttEndpointId) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
}
else if (use_custom_stt && custom_stt_endpoint) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
}
if (this.hints && this.hints.length > 0) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
if (this.altLanguages.length > 1) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
if (this.altLanguages.length > 0) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
else opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;

View File

@@ -42,9 +42,9 @@ const clearChannels = () => {
};
const clearFiles = () => {
const {logger} = require('../..');
const out = execSync('find /tmp -name "*.mp3" -mtime +2 -exec rm {} \\;');
logger.debug({out}, 'clearFiles: command output');
//const {logger} = require('../..');
/*const out = */ execSync('find /tmp -name "*.mp3" -mtime +2 -exec rm {} \\;');
//logger.debug({out}, 'clearFiles: command output');
};

View File

@@ -36,6 +36,8 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.region = o.region;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));

View File

@@ -28,8 +28,8 @@ class HttpRequestor extends BaseRequestor {
assert(['GET', 'POST'].includes(this.method));
const u = this._parsedUrl = parseUrl(this.url);
this._baseUrl = `${u.protocol}://${u.resource}`;
this._resource = u.resource;
if (u.port) this._baseUrl = `${u.protocol}://${u.resource}:${u.port}`;
else this._baseUrl = `${u.protocol}://${u.resource}`;
this._protocol = u.protocol;
this._search = u.search;
this._usePools = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
@@ -49,7 +49,10 @@ class HttpRequestor extends BaseRequestor {
this.logger.debug(`HttpRequestor:created pool for ${this._baseUrl}`);
}
}
else this.client = new Client(`${u.protocol}://${u.resource}`);
else {
if (u.port) this.client = new Client(`${u.protocol}://${u.resource}:${u.port}`);
else this.client = new Client(`${u.protocol}://${u.resource}`);
}
}
get baseUrl() {
@@ -101,7 +104,8 @@ class HttpRequestor extends BaseRequestor {
query = u.query;
}
else {
client = newClient = new Client(`${u.protocol}://${u.resource}`);
if (u.port) client = newClient = new Client(`${u.protocol}://${u.resource}:${u.port}`);
else client = newClient = new Client(`${u.protocol}://${u.resource}`);
path = u.pathname;
query = u.query;
}

View File

@@ -67,7 +67,8 @@ class SingleDialer extends Emitter {
...opts.headers,
...(this.target.headers || {}),
'X-Jambonz-Routing': this.target.type,
'X-Call-Sid': this.callSid
'X-Call-Sid': this.callSid,
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
};
if (srf.locals.fsUUID) {
opts.headers = {

View File

@@ -1,42 +1,7 @@
const bent = require('bent');
const parseUrl = require('parse-url');
const assert = require('assert');
const snakeCaseKeys = require('./snakecase-keys');
const crypto = require('crypto');
const timeSeries = require('@jambonz/time-series');
let alerter ;
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
function computeSignature(payload, timestamp, secret) {
assert(secret);
const data = `${timestamp}.${JSON.stringify(payload)}`;
return crypto
.createHmac('sha256', secret)
.update(data, 'utf8')
.digest('hex');
}
function generateSigHeader(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signature = computeSignature(payload, timestamp, secret);
const scheme = 'v1';
return {
'Jambonz-Signature': `t=${timestamp},${scheme}=${signature}`
};
}
function basicAuth(username, password) {
if (!username || !password) return {};
const creds = `${username}:${password || ''}`;
const header = `Basic ${toBase64(creds)}`;
return {Authorization: header};
}
function isRelativeUrl(u) {
return typeof u === 'string' && u.startsWith('/');
}
function isAbsoluteUrl(u) {
return typeof u === 'string' &&
u.startsWith('https://') || u.startsWith('http://');
@@ -49,14 +14,6 @@ class Requestor {
this.logger = logger;
this.url = hook.url;
this.method = hook.method || 'POST';
this.authHeader = basicAuth(hook.username, hook.password);
const u = parseUrl(this.url);
const myPort = u.port ? `:${u.port}` : '';
const baseUrl = this._baseUrl = `${u.protocol}://${u.resource}${myPort}`;
this.get = bent(baseUrl, 'GET', 'buffer', 200, 201);
this.post = bent(baseUrl, 'POST', 'buffer', 200, 201);
this.username = hook.username;
this.password = hook.password;
@@ -78,72 +35,15 @@ class Requestor {
}
}
get baseUrl() {
return this._baseUrl;
}
/**
* Make an HTTP request.
* All requests use json bodies.
* All requests expect a 200 statusCode on success
* @param {object|string} hook - may be a absolute or relative url, or an object
* @param {string} [hook.url] - an absolute or relative url
* @param {string} [hook.method] - 'GET' or 'POST'
* @param {string} [hook.username] - if basic auth is protecting the endpoint
* @param {string} [hook.password] - if basic auth is protecting the endpoint
* @param {object} [params] - request parameters
*/
async request(hook, params) {
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
const url = hook.url || hook;
const method = hook.method || 'POST';
assert.ok(url, 'Requestor:request url was not provided');
assert.ok, (['GET', 'POST'].includes(method), `Requestor:request method must be 'GET' or 'POST' not ${method}`);
const {url: urlInfo = hook, method: methodInfo = 'POST'} = hook; // mask user/pass
this.logger.debug({url: urlInfo, method: methodInfo, payload}, `Requestor:request ${method} ${url}`);
const startAt = process.hrtime();
let buf;
try {
const sigHeader = generateSigHeader(payload, this.secret);
const headers = {...sigHeader, ...this.authHeader};
//this.logger.info({url, headers}, 'send webhook');
buf = isRelativeUrl(url) ?
await this.post(url, payload, headers) :
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
} catch (err) {
this.logger.error({err, secret: this.secret, baseUrl: this.baseUrl, url, statusCode: err.statusCode},
`web callback returned unexpected error code ${err.statusCode}`);
let opts = {account_sid: this.account_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
}
else if (err.name === 'StatusError') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_STATUS_FAILURE, url, status: err.statusCode};
}
else {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url, detail: err.message};
}
alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
throw err;
}
const diff = process.hrtime(startAt);
const time = diff[0] * 1e3 + diff[1] * 1e-6;
const rtt = time.toFixed(0);
if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']);
if (buf && buf.toString().length > 0) {
try {
const json = JSON.parse(buf.toString());
this.logger.info({response: json}, `Requestor:request ${method} ${url} succeeded in ${rtt}ms`);
return json;
}
catch (err) {
//this.logger.debug({err, url, method}, `Requestor:request returned non-JSON content: '${buf.toString()}'`);
}
get Alerter() {
if (!alerter) {
alerter = timeSeries(this.logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50,
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
}
return alerter;
}
}

View File

@@ -104,8 +104,14 @@ module.exports = (logger) => {
const {srf} = require('../..');
const {addToSet} = srf.locals.dbHelpers;
const uuid = srf.locals.fsUUID = uuidv4();
addToSet(FS_UUID_SET_NAME, uuid)
.catch((err) => logger.info({err}, `Error adding ${uuid} to set ${FS_UUID_SET_NAME}`));
/* in case redis is restarted, re-insert our key every so often */
setInterval(() => {
// eslint-disable-next-line max-len
addToSet(FS_UUID_SET_NAME, uuid).catch((err) => logger.info({err}, `Error adding ${uuid} to set ${FS_UUID_SET_NAME}`));
}, 30000);
// eslint-disable-next-line max-len
addToSet(FS_UUID_SET_NAME, uuid).catch((err) => logger.info({err}, `Error adding ${uuid} to set ${FS_UUID_SET_NAME}`));
});
}
else {

View File

@@ -138,7 +138,7 @@ class WsRequestor extends BaseRequestor {
success: (response) => {
clearTimeout(timer);
const rtt = this._roundTrip(startAt);
this.logger.info({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
this.logger.debug({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']);
resolve(response);
},
@@ -187,7 +187,7 @@ class WsRequestor extends BaseRequestor {
followRedirects: true,
maxRedirects: 2,
handshakeTimeout,
maxPayload: 8096,
maxPayload: process.env.JAMBONES_WS_MAX_PAYLOAD ? parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024,
};
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
@@ -286,7 +286,7 @@ class WsRequestor extends BaseRequestor {
const obj = JSON.parse(content);
const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
this.logger.debug({obj}, 'WsRequestor:request websocket: received');
//this.logger.debug({obj}, 'WsRequestor:request websocket: received');
assert.ok(type, 'type property not supplied');
switch (type) {
@@ -323,7 +323,7 @@ class WsRequestor extends BaseRequestor {
_recvCommand(msgid, command, call_sid, queueCommand, data) {
// TODO: validate command
this.logger.info({msgid, command, call_sid, queueCommand, data}, 'received command');
this.logger.debug({msgid, command, call_sid, queueCommand, data}, 'received command');
this.emit('command', {msgid, command, call_sid, queueCommand, data});
}
}

152
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "jambonz-feature-server",
"version": "v0.7.5",
"version": "v0.7.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jambonz-feature-server",
"version": "v0.7.5",
"version": "v0.7.6",
"license": "MIT",
"dependencies": {
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/realtimedb-helpers": "^0.4.32",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.9",
"@jambonz/time-series": "^0.2.1",
"@opentelemetry/api": "^1.1.0",
"@opentelemetry/exporter-jaeger": "^1.3.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
@@ -27,20 +27,20 @@
"bent": "^7.3.12",
"debug": "^4.3.4",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.2",
"drachtio-fsmrf": "^3.0.3",
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"helmet": "^5.1.0",
"ip": "^1.1.8",
"moment": "^2.29.3",
"parse-url": "^7.0.2",
"moment": "^2.29.4",
"parse-url": "^8.1.0",
"pino": "^6.14.0",
"sdp-transform": "^2.14.1",
"short-uuid": "^4.2.0",
"to-snake-case": "^1.0.0",
"undici": "^5.8.0",
"undici": "^5.8.2",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.7",
"verify-aws-sns-signature": "^0.1.0",
"ws": "^8.8.0",
"xml2js": "^0.4.23"
},
@@ -543,9 +543,9 @@
}
},
"node_modules/@jambonz/realtimedb-helpers": {
"version": "0.4.29",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.29.tgz",
"integrity": "sha512-M31DIbd/QN5IP2727CbEt4C3WvYDHP+X/REepy1bCBnlIyN87fmA32Mjcedxh+5GY/RvtA2iod7EbzJq1LWbNA==",
"version": "0.4.32",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.32.tgz",
"integrity": "sha512-m08rqFEg6I43JVwjiyt0ahvljxJ3YvL3xsKHMoSJw48hdy/8V4w9ovYnUNaZGhaerhJVw36fCfTk4YNlByTnVw==",
"dependencies": {
"@google-cloud/text-to-speech": "^3.4.0",
"@jambonz/promisify-redis": "^0.0.6",
@@ -566,12 +566,12 @@
}
},
"node_modules/@jambonz/time-series": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.9.tgz",
"integrity": "sha512-FLD7mLGEMToG7s6LGWr/GtNp61753RBxyYuKSxCVI6G14bm6ydgcBdG85h8EGTVQlIp9dnwbn5ebSVsGb8a46w==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.1.tgz",
"integrity": "sha512-uAoeZ3ibS7kEOGdT+vaY8BB8hOV4q38eEaF+d5OvLQaHCrPonNiwB8tWhhXDwtYdDompfqVRUy/plNA9fyS7Vw==",
"dependencies": {
"debug": "^4.3.1",
"influx": "^5.8.0"
"influx": "^5.9.3"
}
},
"node_modules/@opentelemetry/api": {
@@ -1992,9 +1992,9 @@
}
},
"node_modules/drachtio-fsmrf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.2.tgz",
"integrity": "sha512-Daub8hXFVNr8OTc7HnkdusNFjpgBavLYr+0TVTu9q5eemum7lgmnrCY3qKfSnPDEb24TpLbZarFa4NWPFYoqNQ==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.3.tgz",
"integrity": "sha512-RJpGS+lX3Cmqd725zJGf1KvONH38/9YDHkPxXevmiVBp+iD/EbRps/ze5wpE0MKbjEA93DOmiXGTWqsOChWttw==",
"dependencies": {
"camel-case": "^4.1.2",
"debug": "^2.6.9",
@@ -3614,14 +3614,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-ssh": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz",
"integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==",
"dependencies": {
"protocols": "^2.0.1"
}
},
"node_modules/is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
@@ -4436,17 +4428,6 @@
"node": ">=8"
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nyc": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
@@ -4672,22 +4653,19 @@
}
},
"node_modules/parse-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-5.0.0.tgz",
"integrity": "sha512-qOpH55/+ZJ4jUu/oLO+ifUKjFPNZGfnPJtzvGzKN/4oLMil5m9OH4VpOj6++9/ytJcfks4kzH2hhi87GL/OU9A==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz",
"integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==",
"dependencies": {
"protocols": "^2.0.0"
}
},
"node_modules/parse-url": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-7.0.2.tgz",
"integrity": "sha512-PqO4Z0eCiQ08Wj6QQmrmp5YTTxpYfONdOEamrtvK63AmzXpcavIVQubGHxOEwiIoDZFb8uDOoQFS0NCcjqIYQg==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz",
"integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==",
"dependencies": {
"is-ssh": "^1.4.0",
"normalize-url": "^6.1.0",
"parse-path": "^5.0.0",
"protocols": "^2.0.1"
"parse-path": "^7.0.0"
}
},
"node_modules/parseurl": {
@@ -5883,9 +5861,9 @@
}
},
"node_modules/undici": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.8.0.tgz",
"integrity": "sha512-1F7Vtcez5w/LwH2G2tGnFIihuWUlc58YidwLiCv+jR2Z50x0tNXpRRw7eOIJ+GvqCqIkg9SB7NWAJ/T9TLfv8Q==",
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.9.1.tgz",
"integrity": "sha512-6fB3a+SNnWEm4CJbgo0/CWR8RGcOCQP68SF4X0mxtYTq2VNN8T88NYrWVBAeSX+zb7bny2dx2iYhP3XHi00omg==",
"engines": {
"node": ">=12.18"
}
@@ -5993,12 +5971,12 @@
}
},
"node_modules/verify-aws-sns-signature": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/verify-aws-sns-signature/-/verify-aws-sns-signature-0.0.7.tgz",
"integrity": "sha512-j/yePIQvLqRGshOwuEs9VT7jGh++1hBoOjjt+Rl4aAffJTu+22GwTPfAD9fLY9VqRrR4Cuiid3eNHOkmxE3TNg==",
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/verify-aws-sns-signature/-/verify-aws-sns-signature-0.1.0.tgz",
"integrity": "sha512-giPj4dIrhounlQA+AAy0CZmqARrT2o4WjIcv1GKcnQiKBDmDpJyGIaHu/ESwOGVcZf68aLHFPrEzhudXSp4Krw==",
"dependencies": {
"bent": "^7.3.12",
"parse-url": "^7.0.2"
"parse-url": "^8.1.0"
}
},
"node_modules/verror": {
@@ -6691,9 +6669,9 @@
}
},
"@jambonz/realtimedb-helpers": {
"version": "0.4.29",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.29.tgz",
"integrity": "sha512-M31DIbd/QN5IP2727CbEt4C3WvYDHP+X/REepy1bCBnlIyN87fmA32Mjcedxh+5GY/RvtA2iod7EbzJq1LWbNA==",
"version": "0.4.32",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.32.tgz",
"integrity": "sha512-m08rqFEg6I43JVwjiyt0ahvljxJ3YvL3xsKHMoSJw48hdy/8V4w9ovYnUNaZGhaerhJVw36fCfTk4YNlByTnVw==",
"requires": {
"@google-cloud/text-to-speech": "^3.4.0",
"@jambonz/promisify-redis": "^0.0.6",
@@ -6714,12 +6692,12 @@
}
},
"@jambonz/time-series": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.9.tgz",
"integrity": "sha512-FLD7mLGEMToG7s6LGWr/GtNp61753RBxyYuKSxCVI6G14bm6ydgcBdG85h8EGTVQlIp9dnwbn5ebSVsGb8a46w==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.1.tgz",
"integrity": "sha512-uAoeZ3ibS7kEOGdT+vaY8BB8hOV4q38eEaF+d5OvLQaHCrPonNiwB8tWhhXDwtYdDompfqVRUy/plNA9fyS7Vw==",
"requires": {
"debug": "^4.3.1",
"influx": "^5.8.0"
"influx": "^5.9.3"
}
},
"@opentelemetry/api": {
@@ -7791,9 +7769,9 @@
}
},
"drachtio-fsmrf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.2.tgz",
"integrity": "sha512-Daub8hXFVNr8OTc7HnkdusNFjpgBavLYr+0TVTu9q5eemum7lgmnrCY3qKfSnPDEb24TpLbZarFa4NWPFYoqNQ==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.3.tgz",
"integrity": "sha512-RJpGS+lX3Cmqd725zJGf1KvONH38/9YDHkPxXevmiVBp+iD/EbRps/ze5wpE0MKbjEA93DOmiXGTWqsOChWttw==",
"requires": {
"camel-case": "^4.1.2",
"debug": "^2.6.9",
@@ -9030,14 +9008,6 @@
"call-bind": "^1.0.2"
}
},
"is-ssh": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz",
"integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==",
"requires": {
"protocols": "^2.0.1"
}
},
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
@@ -9665,11 +9635,6 @@
"process-on-spawn": "^1.0.0"
}
},
"normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
},
"nyc": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
@@ -9838,22 +9803,19 @@
}
},
"parse-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-5.0.0.tgz",
"integrity": "sha512-qOpH55/+ZJ4jUu/oLO+ifUKjFPNZGfnPJtzvGzKN/4oLMil5m9OH4VpOj6++9/ytJcfks4kzH2hhi87GL/OU9A==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz",
"integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==",
"requires": {
"protocols": "^2.0.0"
}
},
"parse-url": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-7.0.2.tgz",
"integrity": "sha512-PqO4Z0eCiQ08Wj6QQmrmp5YTTxpYfONdOEamrtvK63AmzXpcavIVQubGHxOEwiIoDZFb8uDOoQFS0NCcjqIYQg==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz",
"integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==",
"requires": {
"is-ssh": "^1.4.0",
"normalize-url": "^6.1.0",
"parse-path": "^5.0.0",
"protocols": "^2.0.1"
"parse-path": "^7.0.0"
}
},
"parseurl": {
@@ -10783,9 +10745,9 @@
}
},
"undici": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.8.0.tgz",
"integrity": "sha512-1F7Vtcez5w/LwH2G2tGnFIihuWUlc58YidwLiCv+jR2Z50x0tNXpRRw7eOIJ+GvqCqIkg9SB7NWAJ/T9TLfv8Q=="
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.9.1.tgz",
"integrity": "sha512-6fB3a+SNnWEm4CJbgo0/CWR8RGcOCQP68SF4X0mxtYTq2VNN8T88NYrWVBAeSX+zb7bny2dx2iYhP3XHi00omg=="
},
"unix-dgram": {
"version": "2.0.4",
@@ -10869,12 +10831,12 @@
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
"verify-aws-sns-signature": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/verify-aws-sns-signature/-/verify-aws-sns-signature-0.0.7.tgz",
"integrity": "sha512-j/yePIQvLqRGshOwuEs9VT7jGh++1hBoOjjt+Rl4aAffJTu+22GwTPfAD9fLY9VqRrR4Cuiid3eNHOkmxE3TNg==",
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/verify-aws-sns-signature/-/verify-aws-sns-signature-0.1.0.tgz",
"integrity": "sha512-giPj4dIrhounlQA+AAy0CZmqARrT2o4WjIcv1GKcnQiKBDmDpJyGIaHu/ESwOGVcZf68aLHFPrEzhudXSp4Krw==",
"requires": {
"bent": "^7.3.12",
"parse-url": "^7.0.2"
"parse-url": "^8.1.0"
}
},
"verror": {

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-feature-server",
"version": "v0.7.5",
"version": "v0.7.6",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -16,9 +16,7 @@
"type": "git",
"url": "https://github.com/jambonz/jambonz-feature-server.git"
},
"bugs": {
"url": "https://github.com/jambonz/jambonz-feature-server/issues"
},
"bugs": {},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
@@ -28,9 +26,9 @@
"dependencies": {
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/realtimedb-helpers": "^0.4.32",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.9",
"@jambonz/time-series": "^0.2.1",
"@opentelemetry/api": "^1.1.0",
"@opentelemetry/exporter-jaeger": "^1.3.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
@@ -44,20 +42,20 @@
"bent": "^7.3.12",
"debug": "^4.3.4",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.2",
"drachtio-fsmrf": "^3.0.3",
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"helmet": "^5.1.0",
"ip": "^1.1.8",
"moment": "^2.29.3",
"parse-url": "^7.0.2",
"moment": "^2.29.4",
"parse-url": "^8.1.0",
"pino": "^6.14.0",
"sdp-transform": "^2.14.1",
"short-uuid": "^4.2.0",
"to-snake-case": "^1.0.0",
"undici": "^5.8.0",
"undici": "^5.8.2",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.7",
"verify-aws-sns-signature": "^0.1.0",
"ws": "^8.8.0",
"xml2js": "^0.4.23"
},

View File

@@ -2,9 +2,11 @@ const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const clearModule = require('clear-module');
const provisionCallHook = require('./utils')
const {provisionCallHook} = require('./utils')
const getJSON = bent('json')
const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
@@ -23,6 +25,11 @@ test('test create-call timeout', async(t) => {
try {
await connect(srf);
// give UAS app time to come up
const p = sippUac('uas-timeout-cancel.xml', '172.38.0.10');
await waitFor(1000);
// GIVEN
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
@@ -39,7 +46,7 @@ test('test create-call timeout', async(t) => {
"number": "15583084809"
}});
//THEN
await sippUac('uas-timeout-cancel.xml', '172.38.0.10');
await p;
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
@@ -54,10 +61,16 @@ test('test create-call call-hook basic authentication', async(t) => {
try {
await connect(srf);
// GIVEN
let from = 'call_hook_basic_authentication';
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
// Give UAS app time to come up
const p = sippUac('uas.xml', '172.38.0.10', from);
await waitFor(1000);
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
@@ -81,7 +94,7 @@ test('test create-call call-hook basic authentication', async(t) => {
];
provisionCallHook(from, verbs);
//THEN
await sippUac('uas.xml', '172.38.0.10', from);
await p;
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}`)
t.ok(obj.headers.Authorization = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',

View File

@@ -3,7 +3,7 @@ const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
const provisionCallHook = require('./utils')
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);

View File

@@ -7,5 +7,6 @@ require('./say-tests');
require('./gather-tests');
require('./sip-request-tests');
require('./create-call-test');
require('./play-tests');
require('./remove-test-db');
require('./docker_stop');

198
test/play-tests.js Normal file
View File

@@ -0,0 +1,198 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook, provisionCustomHook} = require('./utils')
const bent = require('bent');
const getJSON = bent('json')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'play\' tests single link in plain text', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'https://example.com/example.mp3'
}
];
const from = 'play_single_link';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using single link');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests multi links in array', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: ['https://example.com/example.mp3', 'https://example.com/example.mp3']
}
];
const from = 'play_multi_links_in_array';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using links in array');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests single link in conference', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const from = 'play_single_link_in_conference';
const waitHookVerbs = [
{
verb: 'play',
url: 'https://example.com/example.mp3'
}
];
const verbs = [
{
verb: 'conference',
name: `${from}`,
beep: true,
"startConferenceOnEnter": false,
waitHook: `/customHook`
}
];
provisionCustomHook(from, waitHookVerbs)
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using in conference as single link');
// Make sure that waitHook is called and success
await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests multi links in array in conference', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const from = 'play_multi_links_in_conference';
const waitHookVerbs = [
{
verb: 'play',
url: ['https://example.com/example.mp3', 'https://example.com/example.mp3']
}
];
const verbs = [
{
verb: 'conference',
name: `${from}`,
beep: true,
"startConferenceOnEnter": false,
waitHook: `/customHook`
}
];
provisionCustomHook(from, waitHookVerbs)
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using in conference with multi links');
// Make sure that waitHook is called and success
await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests with seekOffset and actionHook', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'silence_stream://5000',
seekOffset: 8000,
timeoutSecs: 2,
actionHook: '/customHook'
}
];
const waitHookVerbs = [];
const from = 'play_action_hook';
provisionCallHook(from, verbs)
provisionCustomHook(from, waitHookVerbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds');
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
t.ok(obj.body.reason === "playCompleted", "play: actionHook success received")
t.ok(obj.body.playback_seconds === "2", "playback_seconds: actionHook success received")
t.ok(obj.body.playback_milliseconds === "2048", "playback_milliseconds: actionHook success received")
t.ok(obj.body.playback_last_offset_pos === "16000", "playback_last_offset_pos: actionHook success received")
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,7 +1,7 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const provisionCallHook = require('./utils')
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="480" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<pause milliseconds="3000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>

View File

@@ -3,7 +3,7 @@ const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
const provisionCallHook = require('./utils');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);

View File

@@ -63,7 +63,7 @@ obj.sippUac = (file, bindAddress, from='sipp', to='16174000000') => {
addOutput(data.toString());
});
child_process.stdout.on('data', (data) => {
//console.log(`stdout: ${data}`);
// console.log(`stdout: ${data}`);
addOutput(data.toString());
});
});

View File

@@ -15,4 +15,13 @@ const provisionCallHook = (from, verbs) => {
post('/appMapping', mapping);
}
module.exports = provisionCallHook
const provisionCustomHook = (from, verbs) => {
const mapping = {
from,
data: JSON.stringify(verbs)
};
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
post(`/customHookMapping`, mapping);
}
module.exports = { provisionCallHook, provisionCustomHook}

View File

@@ -20,11 +20,7 @@ app.use(express.json());
app.all('/', (req, res) => {
console.log(req.body, 'POST /');
const key = req.body.from
if (!json_mapping.has(key)) return res.sendStatus(404);
const retData = JSON.parse(json_mapping.get(key));
console.log(retData, `${req.method} /`);
addRequestToMap(key, req, hook_mapping);
return res.json(retData);
return getJsonFromMap(key, req, res);
});
app.post('/appMapping', (req, res) => {
@@ -52,6 +48,24 @@ app.post('/actionHook', (req, res) => {
return res.sendStatus(200);
});
/*
* customHook
* For the hook to return
*/
app.all('/customHook', (req, res) => {
let key = `${req.body.from}_customHook`;;
console.log(req.body, `POST /customHook`);
return getJsonFromMap(key, req, res);
});
app.post('/customHookMapping', (req, res) => {
let key = `${req.body.from}_customHook`;
console.log(req.body, `POST /customHookMapping`);
json_mapping.set(key, req.body.data);
return res.sendStatus(200);
});
// Fetch Requests
app.get('/requests/:key', (req, res) => {
let key = req.params.key;
@@ -77,6 +91,14 @@ app.get('/lastRequest/:key', (req, res) => {
* private function
*/
function getJsonFromMap(key, req, res) {
if (!json_mapping.has(key)) return res.sendStatus(404);
const retData = JSON.parse(json_mapping.get(key));
console.log(retData, ` Response to ${req.method} ${req.url}`);
addRequestToMap(key, req, hook_mapping);
return res.json(retData);
}
function addRequestToMap(key, req, map) {
let headers = new Map()
for(let i = 0; i < req.rawHeaders.length; i++) {

View File

@@ -1,6 +1,15 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
const opts = {
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const logger = require('pino')(opts);
const { queryAlerts } = require('@jambonz/time-series')(
logger, process.env.JAMBONES_TIME_SERIES_HOST
);
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -17,7 +26,6 @@ function connect(connectable) {
test('basic webhook tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
const provisionCallHook = require('./utils')
try {
await connect(srf);
@@ -28,7 +36,7 @@ test('basic webhook tests', async(t) => {
reason: 'Gone Fishin',
headers: {
'Retry-After': 300
}
}
}
];
@@ -45,3 +53,43 @@ test('basic webhook tests', async(t) => {
t.error(err);
}
});
test('invalid jambonz json create alert tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
// Invalid json array
const verbs = {
verb: 'say',
text: 'hello'
};
const from = 'invalid_json_create_alert';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-invite-expect-480.xml', '172.38.0.10', from);
// sleep testcase for more than 7 second to wait alert pushed to database.
await sleep(8000);
const data = await queryAlerts(
{account_sid: 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f', page: 1, page_size: 25, days: 7});
let checked = false;
for (let i = 0; i < data.total; i++) {
checked = data.data[i].message === 'malformed jambonz payload: must be array'
}
t.ok(checked, 'alert is raised as expected');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}