mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-09 02:30:17 +00:00
Compare commits
56 Commits
snyk-fix-5
...
v0.7.7-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
138950c534 | ||
|
|
215a28b615 | ||
|
|
3a5efa37b9 | ||
|
|
917b8f332c | ||
|
|
17848ea22c | ||
|
|
43af27e802 | ||
|
|
b25f92e17a | ||
|
|
90cb5e1348 | ||
|
|
cf821569b3 | ||
|
|
218f2d6c67 | ||
|
|
c2c8f00978 | ||
|
|
32714d73f3 | ||
|
|
8da85ebd5a | ||
|
|
dcedf68264 | ||
|
|
05c5d2211f | ||
|
|
0c089e2380 | ||
|
|
099f33857c | ||
|
|
bd49dacac4 | ||
|
|
876824abde | ||
|
|
468a9e6d6b | ||
|
|
c88163fe11 | ||
|
|
bf7ece8f17 | ||
|
|
e90ef6bc70 | ||
|
|
a59f6097d7 | ||
|
|
887c6243e2 | ||
|
|
127432f2ec | ||
|
|
4f0439dad9 | ||
|
|
9c188736f9 | ||
|
|
a69dbb3d4f | ||
|
|
b2e21f06a8 | ||
|
|
a325bb554a | ||
|
|
aa5e3d9437 | ||
|
|
6346954e7a | ||
|
|
5b6f7dd3ee | ||
|
|
7199db5edb | ||
|
|
8644b858b3 | ||
|
|
3d475217ca | ||
|
|
f580bc60f5 | ||
|
|
1a4f8563f2 | ||
|
|
a021ca19a5 | ||
|
|
2a4f8e3ff9 | ||
|
|
3298918322 | ||
|
|
f068aa5390 | ||
|
|
6e6ab56163 | ||
|
|
91204955c9 | ||
|
|
bc3552dda7 | ||
|
|
d459be2942 | ||
|
|
1c5c76de61 | ||
|
|
cb6817449d | ||
|
|
ffa006225b | ||
|
|
11d9a13ac7 | ||
|
|
21d5af367b | ||
|
|
2882fa2d0a | ||
|
|
a035b67e6c | ||
|
|
6979affb86 | ||
|
|
bb9c3a8df0 |
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:16.15.1-alpine as base
|
||||
FROM --platform=linux/amd64 node:18.9.0-alpine3.16 as base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
|
||||
17
app.js
17
app.js
@@ -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);
|
||||
@@ -28,6 +26,7 @@ installSrfLocals(srf, logger);
|
||||
const {
|
||||
initLocals,
|
||||
createRootSpan,
|
||||
handleSipRec,
|
||||
getAccountDetails,
|
||||
normalizeNumbers,
|
||||
retrieveApplication,
|
||||
@@ -46,6 +45,7 @@ Object.assign(app.locals, {
|
||||
const httpRoutes = require('./lib/http-routes');
|
||||
|
||||
const InboundCallSession = require('./lib/session/inbound-call-session');
|
||||
const SipRecCallSession = require('./lib/session/siprec-call-session');
|
||||
|
||||
if (process.env.DRACHTIO_HOST) {
|
||||
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
|
||||
@@ -68,14 +68,17 @@ if (process.env.NODE_ENV === 'test') {
|
||||
srf.use('invite', [
|
||||
initLocals,
|
||||
createRootSpan,
|
||||
handleSipRec,
|
||||
getAccountDetails,
|
||||
normalizeNumbers,
|
||||
retrieveApplication,
|
||||
invokeWebCallback
|
||||
]);
|
||||
|
||||
srf.invite((req, res) => {
|
||||
const session = new InboundCallSession(req, res);
|
||||
srf.invite(async(req, res) => {
|
||||
const isSipRec = !!req.locals.siprec;
|
||||
const session = isSipRec ? new SipRecCallSession(req, res) : new InboundCallSession(req, res);
|
||||
if (isSipRec) await session.answerSipRecCall();
|
||||
session.exec();
|
||||
});
|
||||
|
||||
|
||||
52
data/example-voicemail-greetings.json
Normal file
52
data/example-voicemail-greetings.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"en-US": [
|
||||
"call has been forwarded",
|
||||
"at the beep",
|
||||
"at the tone",
|
||||
"leave a message",
|
||||
"leave me a message",
|
||||
"not available right now",
|
||||
"not available to take your call",
|
||||
"can't take your call",
|
||||
"I will get back to you",
|
||||
"I'll get back to you",
|
||||
"we will get back to you",
|
||||
"we are unable",
|
||||
"we are not available"
|
||||
],
|
||||
"es-ES": [
|
||||
"le pasamos la llamada",
|
||||
"después del bip",
|
||||
"después del tono",
|
||||
"deja un mensaje",
|
||||
"déjame un mensaje",
|
||||
"no estamos disponibles",
|
||||
"no estoy disponible",
|
||||
"ahora no puedo",
|
||||
"no puedo contestar",
|
||||
"no le puedo contestar",
|
||||
"me pondré en contacto",
|
||||
"nos pondremos en contacto",
|
||||
"ahora no estamos disponibles",
|
||||
"no estamos disponibles"
|
||||
],
|
||||
"ca-ES": [
|
||||
"passem la seva trucada",
|
||||
"després del bip",
|
||||
"després del to",
|
||||
"deixi un missatge",
|
||||
"deixa un missatge",
|
||||
"deixim un missatge",
|
||||
"no estem disponibles",
|
||||
"no estem a l'oficina",
|
||||
"no estic disponible",
|
||||
"ara no puc",
|
||||
"no puc contestar",
|
||||
"no puc respondre",
|
||||
"no li puc respondre",
|
||||
"em posaré en contacte",
|
||||
"ens posarem en contacto",
|
||||
"ara no estem disponibles",
|
||||
"no hi som"
|
||||
],
|
||||
}
|
||||
@@ -84,7 +84,6 @@ router.post('/', async(req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* create endpoint for outdial */
|
||||
const ms = getFreeswitch();
|
||||
if (!ms) throw new Error('no available Freeswitch for outbound call creation');
|
||||
@@ -197,9 +196,10 @@ router.post('/', async(req, res) => {
|
||||
});
|
||||
cs.exec(req);
|
||||
|
||||
res.status(201).json({sid: cs.callSid});
|
||||
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')});
|
||||
|
||||
sipLogger.info(`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
|
||||
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')},
|
||||
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
|
||||
},
|
||||
cbProvisional: (prov) => {
|
||||
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const {CallDirection} = require('./utils/constants');
|
||||
const {CallDirection, AllowedSipRecVerbs} = require('./utils/constants');
|
||||
const {parseSiprecPayload} = require('./utils/siprec-utils');
|
||||
const CallInfo = require('./session/call-info');
|
||||
const HttpRequestor = require('./utils/http-requestor');
|
||||
const WsRequestor = require('./utils/ws-requestor');
|
||||
@@ -18,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}`);
|
||||
@@ -40,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;
|
||||
|
||||
@@ -48,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,
|
||||
@@ -73,6 +80,35 @@ module.exports = function(srf, logger) {
|
||||
next();
|
||||
}
|
||||
|
||||
const handleSipRec = async(req, res, next) => {
|
||||
if (Array.isArray(req.payload) && req.payload.length > 1) {
|
||||
const {callId, logger} = req.locals;
|
||||
logger.debug({payload: req.payload}, 'handling siprec call');
|
||||
|
||||
try {
|
||||
const sdp = req.payload
|
||||
.find((p) => p.type === 'application/sdp')
|
||||
.content;
|
||||
const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger);
|
||||
req.locals.calledNumber = metadata.caller.number;
|
||||
req.locals.callingNumber = metadata.callee.number;
|
||||
req.locals = {
|
||||
...req.locals,
|
||||
siprec: {
|
||||
metadata,
|
||||
sdp1,
|
||||
sdp2
|
||||
}
|
||||
};
|
||||
logger.info({callId, metadata, sdp}, 'successfully parsed SIPREC payload');
|
||||
} catch (err) {
|
||||
logger.info({callId}, 'Error parsing multipart payload');
|
||||
return res.send(503);
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* retrieve account information for the incoming call
|
||||
*/
|
||||
@@ -101,7 +137,10 @@ module.exports = function(srf, logger) {
|
||||
* Within the system, we deal with E.164 numbers _without_ the leading '+
|
||||
*/
|
||||
function normalizeNumbers(req, res, next) {
|
||||
const logger = req.locals.logger;
|
||||
const {logger, siprec} = req.locals;
|
||||
|
||||
if (siprec) return next();
|
||||
|
||||
Object.assign(req.locals, {
|
||||
calledNumber: req.calledNumber,
|
||||
callingNumber: req.callingNumber
|
||||
@@ -122,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;
|
||||
@@ -225,10 +263,10 @@ module.exports = function(srf, logger) {
|
||||
*/
|
||||
async function invokeWebCallback(req, res, next) {
|
||||
const logger = req.locals.logger;
|
||||
const {rootSpan, application:app} = req.locals;
|
||||
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();
|
||||
@@ -261,10 +299,28 @@ module.exports = function(srf, logger) {
|
||||
});
|
||||
span.end();
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
|
||||
if (siprec) {
|
||||
const tasks = app.tasks.filter((t) => AllowedSipRecVerbs.includes(t.name));
|
||||
if (0 === tasks.length) {
|
||||
logger.info({tasks: app.tasks}, 'no valid verbs in app found for an incoming siprec call');
|
||||
throw new Error('invalid verbs for incoming siprec call');
|
||||
}
|
||||
if (tasks.length < app.tasks.length) {
|
||||
logger.info('removing verbs that are not allowed for incoming siprec call');
|
||||
app.tasks = tasks;
|
||||
}
|
||||
}
|
||||
next();
|
||||
} 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();
|
||||
@@ -274,6 +330,7 @@ module.exports = function(srf, logger) {
|
||||
return {
|
||||
initLocals,
|
||||
createRootSpan,
|
||||
handleSipRec,
|
||||
getAccountDetails,
|
||||
normalizeNumbers,
|
||||
retrieveApplication,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,8 @@ const {
|
||||
CallStatus,
|
||||
TaskName,
|
||||
KillReason,
|
||||
RecordState
|
||||
RecordState,
|
||||
AllowedSipRecVerbs
|
||||
} = require('../utils/constants');
|
||||
const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
@@ -72,6 +73,10 @@ class CallSession extends Emitter {
|
||||
|
||||
if (!this.isConfirmCallSession && !this.isSmsCallSession && !this.isAdultingCallSession) {
|
||||
sessionTracker.add(this.callSid, this);
|
||||
|
||||
const {startAmd, stopAmd} = require('../utils/amd-utils')(logger);
|
||||
this.startAmd = startAmd;
|
||||
this.stopAmd = stopAmd;
|
||||
}
|
||||
|
||||
this._pool = srf.locals.dbHelpers.pool;
|
||||
@@ -224,6 +229,13 @@ class CallSession extends Emitter {
|
||||
return this.constructor.name === 'ConfirmCallSession';
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if this session is a SipRecCallSession
|
||||
*/
|
||||
get isSipRecCallSession() {
|
||||
return this.constructor.name === 'SipRecCallSession';
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if this session is a SmsCallSession
|
||||
*/
|
||||
@@ -249,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');
|
||||
@@ -398,6 +447,7 @@ class CallSession extends Emitter {
|
||||
try {
|
||||
const t = normalizeJambones(this.logger, [gather]);
|
||||
this.backgroundGatherTask = makeTask(this.logger, t[0]);
|
||||
this._bargeInEnabled = true;
|
||||
this.backgroundGatherTask
|
||||
.once('dtmf', this._clearTasks.bind(this))
|
||||
.once('vad', this._clearTasks.bind(this))
|
||||
@@ -414,7 +464,7 @@ class CallSession extends Emitter {
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.span.end();
|
||||
this.backgroundGatherTask = null;
|
||||
if (autoEnable && !this.callGone && !this._stopping) {
|
||||
if (autoEnable && !this.callGone && !this._stopping && this._bargeInEnabled) {
|
||||
this.logger.info('CallSession:enableBotMode: restarting background gather');
|
||||
setImmediate(() => this.enableBotMode(gather, true));
|
||||
}
|
||||
@@ -431,6 +481,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
disableBotMode() {
|
||||
this._bargeInEnabled = false;
|
||||
if (this.backgroundGatherTask) {
|
||||
try {
|
||||
this.backgroundGatherTask.removeAllListeners();
|
||||
@@ -498,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) {
|
||||
@@ -587,6 +640,7 @@ class CallSession extends Emitter {
|
||||
this._onTasksDone();
|
||||
this._clearResources();
|
||||
|
||||
|
||||
if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid);
|
||||
}
|
||||
|
||||
@@ -904,6 +958,20 @@ class CallSession extends Emitter {
|
||||
this.logger.debug('CallSession:replaceApplication - ignoring because call is gone');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSipRecCallSession) {
|
||||
const pruned = tasks.filter((t) => AllowedSipRecVerbs.includes(t.name));
|
||||
if (0 === pruned.length) {
|
||||
this.logger.info({tasks},
|
||||
'CallSession:replaceApplication - only config, transcribe and/or listen allowed on an incoming siprec call');
|
||||
return;
|
||||
}
|
||||
if (pruned.length < tasks.length) {
|
||||
this.logger.info(
|
||||
'CallSession:replaceApplication - removing verbs that are not allowed for incoming siprec call');
|
||||
tasks = pruned;
|
||||
}
|
||||
}
|
||||
this.tasks = tasks;
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx++;
|
||||
@@ -1071,21 +1139,34 @@ 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) {
|
||||
if (task.earlyMedia === true || this.dlg) return this.ep;
|
||||
const resources = {ep: this.ep};
|
||||
if (task.earlyMedia === true || this.dlg) {
|
||||
return {
|
||||
...resources,
|
||||
...(this.isSipRecCallSession && {ep2: this.ep2})
|
||||
};
|
||||
}
|
||||
|
||||
// we are going from an early media connection to answer
|
||||
await this.propagateAnswer();
|
||||
return this.ep;
|
||||
return {
|
||||
...resources,
|
||||
...(this.isSipRecCallSession && {ep2: this.ep2})
|
||||
};
|
||||
}
|
||||
|
||||
// need to allocate an endpoint
|
||||
try {
|
||||
if (!this.ms) this.ms = this.getMS();
|
||||
const ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
const ep = await this.ms.createEndpoint({
|
||||
headers: {
|
||||
'X-Jambones-Call-ID': this.callId,
|
||||
},
|
||||
remoteSdp: this.req.body
|
||||
});
|
||||
//ep.cs = this;
|
||||
this.ep = ep;
|
||||
ep.set({
|
||||
@@ -1102,7 +1183,7 @@ class CallSession extends Emitter {
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
if (task.earlyMedia && !this.req.finalResponseSent) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
return ep;
|
||||
return {ep};
|
||||
}
|
||||
this.logger.debug('propogating answer');
|
||||
await this.propagateAnswer();
|
||||
@@ -1111,10 +1192,11 @@ class CallSession extends Emitter {
|
||||
// outbound call TODO
|
||||
}
|
||||
|
||||
return ep;
|
||||
return {ep};
|
||||
} 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,
|
||||
@@ -1136,7 +1218,7 @@ class CallSession extends Emitter {
|
||||
_evalStableCallPrecondition(task) {
|
||||
if (this.callGone) throw new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
if (!this.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered`);
|
||||
return this.dlg;
|
||||
return {dlg: this.dlg};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1175,7 +1257,7 @@ class CallSession extends Emitter {
|
||||
* Hang up the call and free the media endpoint
|
||||
*/
|
||||
_clearResources() {
|
||||
for (const resource of [this.dlg, this.ep]) {
|
||||
for (const resource of [this.dlg, this.ep, this.ep2]) {
|
||||
if (resource && resource.connected) resource.destroy();
|
||||
}
|
||||
this.dlg = null;
|
||||
@@ -1229,7 +1311,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
|
||||
});
|
||||
@@ -1383,7 +1466,8 @@ class CallSession extends Emitter {
|
||||
headers: {
|
||||
'Refer-To': referTo,
|
||||
'Referred-By': `sip:${this.srf.locals.localSipAddress}`,
|
||||
'X-Retain-Call-Sid': this.callSid
|
||||
'X-Retain-Call-Sid': this.callSid,
|
||||
'X-Account-Sid': this.accountSid
|
||||
}
|
||||
});
|
||||
if ([200, 202].includes(res.status)) {
|
||||
@@ -1421,8 +1505,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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
59
lib/session/siprec-call-session.js
Normal file
59
lib/session/siprec-call-session.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const InboundCallSession = require('./inbound-call-session');
|
||||
const {createSipRecPayload} = require('../utils/siprec-utils');
|
||||
const {CallStatus} = require('../utils/constants');
|
||||
/**
|
||||
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is
|
||||
* established for an inbound SIPREC call.
|
||||
* @extends InboundCallSession
|
||||
*/
|
||||
class SipRecCallSession extends InboundCallSession {
|
||||
constructor(req, res) {
|
||||
super(req, res);
|
||||
|
||||
const {sdp1, sdp2, metadata} = req.locals.siprec;
|
||||
this.sdp1 = sdp1;
|
||||
this.sdp2 = sdp2;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
async answerSipRecCall() {
|
||||
try {
|
||||
this.ms = this.getMS();
|
||||
let remoteSdp = this.sdp1.replace(/sendonly/, 'sendrecv');
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp});
|
||||
//this.logger.debug({remoteSdp, localSdp: this.ep.local.sdp}, 'SipRecCallSession - allocated first endpoint');
|
||||
remoteSdp = this.sdp2.replace(/sendonly/, 'sendrecv');
|
||||
this.ep2 = await this.ms.createEndpoint({remoteSdp});
|
||||
//this.logger.debug({remoteSdp, localSdp: this.ep2.local.sdp}, 'SipRecCallSession - allocated second endpoint');
|
||||
await this.ep.bridge(this.ep2);
|
||||
const combinedSdp = await createSipRecPayload(this.ep.local.sdp, this.ep2.local.sdp, this.logger);
|
||||
/*
|
||||
this.logger.debug({
|
||||
combinedSdp
|
||||
}, 'SipRecCallSession:_answerSipRecCall - created SIPREC payload');
|
||||
*/
|
||||
this.dlg = await this.srf.createUAS(this.req, this.res, {
|
||||
headers: {
|
||||
'Content-Type': 'application/sdp',
|
||||
'X-Trace-ID': this.req.locals.traceId,
|
||||
'X-Call-Sid': this.req.locals.callSid,
|
||||
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
|
||||
},
|
||||
localSdp: combinedSdp
|
||||
});
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.wrapDialog(this.dlg);
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.emit('callStatusChange', {sipStatus: 200, sipReason: 'OK', callStatus: CallStatus.InProgress});
|
||||
|
||||
this.dlg.on('modify', this._onReinvite.bind(this));
|
||||
this.dlg.on('refer', this._onRefer.bind(this));
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'SipRecCallSession:_answerSipRecCall error:');
|
||||
if (this.res && !this.res.finalResponseSent) this.res.send(500);
|
||||
this._callReleased();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SipRecCallSession;
|
||||
@@ -72,7 +72,7 @@ class Conference extends Task {
|
||||
get shouldRecord() { return this.record.path; }
|
||||
get isRecording() { return this.recordingInProgress; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
const dlg = cs.dlg;
|
||||
@@ -453,7 +453,7 @@ class Conference extends Task {
|
||||
this._playSession = null;
|
||||
break;
|
||||
}
|
||||
} while (!this.killed && this.conf_hold_status !== 'hold');
|
||||
} while (!this.killed && this.conf_hold_status === 'hold');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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,
|
||||
|
||||
@@ -4,8 +4,6 @@ const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
class TaskConfig extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'synthesizer',
|
||||
'recognizer',
|
||||
@@ -28,7 +26,9 @@ class TaskConfig extends Task {
|
||||
});
|
||||
}
|
||||
if (this.bargeIn.sticky) this.autoEnable = true;
|
||||
this.preconditions = this.bargeIn.enable ? TaskPreconditions.Endpoint : TaskPreconditions.None;
|
||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.data.amd) ?
|
||||
TaskPreconditions.Endpoint :
|
||||
TaskPreconditions.None;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Config; }
|
||||
@@ -50,12 +50,26 @@ class TaskConfig extends Task {
|
||||
const s = `{${v},${l}}`;
|
||||
phrase.push(`set recognizer${s}`);
|
||||
}
|
||||
if (this.data.amd) phrase.push('enable amd');
|
||||
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) {
|
||||
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
|
||||
? this.synthesizer.vendor
|
||||
@@ -80,13 +94,27 @@ 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
|
||||
}, 'Config: updated recognizer');
|
||||
}
|
||||
if ('enable' in this.bargeIn) {
|
||||
if (this.gatherOpts) {
|
||||
if (this.bargeIn.enable === true && this.gatherOpts) {
|
||||
this.gatherOpts.recognizer = this.hasRecognizer ?
|
||||
this.recognizer :
|
||||
{
|
||||
@@ -96,16 +124,29 @@ class TaskConfig extends Task {
|
||||
this.logger.info({opts: this.gatherOpts}, 'Config: enabling bargeIn');
|
||||
cs.enableBotMode(this.gatherOpts, this.autoEnable);
|
||||
}
|
||||
else {
|
||||
else if (this.bargeIn.enable === false) {
|
||||
this.logger.info('Config: disabling bargeIn');
|
||||
cs.disableBotMode();
|
||||
}
|
||||
}
|
||||
if (this.record.action) cs.notifyRecordOptions(this.record);
|
||||
if (this.record.action) {
|
||||
try {
|
||||
await cs.notifyRecordOptions(this.record);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Config: error starting recording');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
|
||||
}
|
||||
|
||||
_onAmdEvent(cs, evt) {
|
||||
this.logger.info({evt}, 'Config:_onAmdEvent');
|
||||
const {actionHook} = this.data.amd;
|
||||
this.performHook(cs, actionHook, evt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class TaskDequeue extends Task {
|
||||
|
||||
get name() { return TaskName.Dequeue; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
this.queueName = `queue:${cs.accountSid}:${this.queueName}`;
|
||||
|
||||
@@ -134,7 +134,10 @@ class TaskDial extends Task {
|
||||
get name() { return TaskName.Dial; }
|
||||
|
||||
get canReleaseMedia() {
|
||||
return !process.env.ANCHOR_MEDIA_ALWAYS && !this.listenTask && !this.transcribeTask;
|
||||
return !process.env.ANCHOR_MEDIA_ALWAYS &&
|
||||
!this.listenTask &&
|
||||
!this.transcribeTask &&
|
||||
!this.startAmd;
|
||||
}
|
||||
|
||||
get summary() {
|
||||
@@ -158,6 +161,11 @@ class TaskDial extends Task {
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
try {
|
||||
if (this.data.amd) {
|
||||
this.startAmd = cs.startAmd;
|
||||
this.stopAmd = cs.stopAmd;
|
||||
this.on('amd', this._onAmdEvent.bind(this, cs));
|
||||
}
|
||||
if (cs.direction === CallDirection.Inbound) {
|
||||
await this._initializeInbound(cs);
|
||||
}
|
||||
@@ -181,6 +189,11 @@ class TaskDial extends Task {
|
||||
|
||||
async kill(cs, reason) {
|
||||
super.kill(cs);
|
||||
try {
|
||||
if (this.ep && this.ep.amd) this.stopAmd(this.ep, this);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'DialTask:kill - error stopping answering machine detectin');
|
||||
}
|
||||
if (this.dialMusic && this.epOther) {
|
||||
this.epOther.api('uuid_break', this.epOther.uuid)
|
||||
.catch((err) => this.logger.info(err, 'Error killing dialMusic'));
|
||||
@@ -203,8 +216,14 @@ class TaskDial extends Task {
|
||||
this.sd = null;
|
||||
}
|
||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||
if (this.listenTask) await this.listenTask.kill(cs);
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
if (this.listenTask) {
|
||||
await this.listenTask.kill(cs);
|
||||
this.listenTask = null;
|
||||
}
|
||||
if (this.transcribeTask) {
|
||||
await this.transcribeTask.kill(cs);
|
||||
this.transcribeTask = null;
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -226,10 +245,10 @@ class TaskDial extends Task {
|
||||
this.logger.debug('Dial:whisper executing tasks');
|
||||
while (tasks.length && !cs.callGone) {
|
||||
const task = tasks.shift();
|
||||
const {span, ctx} = this.startChildSpan(`whisper:${this.sayTask.summary}`);
|
||||
const {span, ctx} = this.startChildSpan(`whisper:${task.summary}`);
|
||||
task.span = span;
|
||||
task.ctx = ctx;
|
||||
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
|
||||
await task.exec(cs, callSid === this.callSid ? {ep: this.ep} : {ep: this.epOther});
|
||||
span.end();
|
||||
}
|
||||
this.logger.debug('Dial:whisper tasks complete');
|
||||
@@ -357,9 +376,8 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
async _initializeInbound(cs) {
|
||||
const ep = await cs._evalEndpointPrecondition(this);
|
||||
const {ep} = await cs._evalEndpointPrecondition(this);
|
||||
this.epOther = ep;
|
||||
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
|
||||
|
||||
/* send outbound legs back to the same SBC (to support static IP feature) */
|
||||
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port}`;
|
||||
@@ -571,6 +589,7 @@ class TaskDial extends Task {
|
||||
* - save the dialog and endpoint
|
||||
* - clock the start time of the call,
|
||||
* - start a max call length timer (optionally)
|
||||
* - start answering machine detection (optionally)
|
||||
* - launch any nested tasks
|
||||
* - and establish a handler to clean up if the called party hangs up
|
||||
*/
|
||||
@@ -611,11 +630,18 @@ class TaskDial extends Task {
|
||||
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
||||
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.epOther, this.ep);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.epOther);
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep2: this.epOther, ep:this.ep});
|
||||
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther});
|
||||
if (this.startAmd) {
|
||||
try {
|
||||
this.startAmd(cs, this.ep, this, this.data.amd);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Dial:_selectSingleDial - Error calling startAmd');
|
||||
}
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
@@ -659,6 +685,12 @@ class TaskDial extends Task {
|
||||
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
||||
res.send(200, {body: sdp});
|
||||
}
|
||||
|
||||
_onAmdEvent(cs, evt) {
|
||||
this.logger.info({evt}, 'Dial:_onAmdEvent');
|
||||
const {actionHook} = this.data.amd;
|
||||
this.performHook(cs, actionHook, evt);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskDial;
|
||||
|
||||
@@ -64,7 +64,7 @@ class Dialogflow extends Task {
|
||||
|
||||
get name() { return TaskName.Dialogflow; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,7 @@ class TaskDtmf extends Task {
|
||||
|
||||
get name() { return TaskName.Dtmf; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
try {
|
||||
|
||||
@@ -37,7 +37,7 @@ class TaskEnqueue extends Task {
|
||||
|
||||
get name() { return TaskName.Enqueue; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
const dlg = cs.dlg;
|
||||
this.queueName = `queue:${cs.accountSid}:${this.queueName}`;
|
||||
|
||||
@@ -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);
|
||||
@@ -27,7 +40,7 @@ class TaskGather extends Task {
|
||||
|
||||
/* timeout of zero means no timeout */
|
||||
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
|
||||
this.interim = this.partialResultHook || this.bargein;
|
||||
this.interim = !!this.partialResultHook || this.bargein;
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 0;
|
||||
if (this.data.recognizer) {
|
||||
@@ -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;
|
||||
@@ -116,11 +130,26 @@ class TaskGather extends Task {
|
||||
return s;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
this.logger.debug('Gather:exec');
|
||||
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;
|
||||
@@ -164,7 +193,7 @@ class TaskGather extends Task {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`);
|
||||
this.sayTask.span = span;
|
||||
this.sayTask.ctx = ctx;
|
||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.on('playDone', (err) => {
|
||||
span.end();
|
||||
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
@@ -176,7 +205,7 @@ class TaskGather extends Task {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
|
||||
this.playTask.span = span;
|
||||
this.playTask.ctx = ctx;
|
||||
this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.on('playDone', (err) => {
|
||||
span.end();
|
||||
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
@@ -273,6 +302,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
if ('google' === this.vendor) {
|
||||
this.bugname = 'google_transcribe';
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
[
|
||||
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
|
||||
@@ -284,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;
|
||||
}
|
||||
@@ -308,6 +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_transcribe';
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
if (this.vocabularyFilterName) {
|
||||
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
|
||||
@@ -324,18 +357,30 @@ class TaskGather extends Task {
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
else if ('microsoft' === this.vendor) {
|
||||
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;
|
||||
@@ -355,12 +400,14 @@ class TaskGather extends Task {
|
||||
this.logger.debug({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
interim: this.interim
|
||||
interim: this.interim,
|
||||
bugname: this.bugname
|
||||
}, 'Gather:_startTranscribing');
|
||||
ep.startTranscription({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
interim: this.interim,
|
||||
bugname: this.bugname,
|
||||
}).catch((err) => {
|
||||
const {writeAlerts, AlertType} = this.cs.srf.locals;
|
||||
this.logger.error(err, 'TaskGather:_startTranscribing error');
|
||||
@@ -440,7 +487,12 @@ class TaskGather extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, evt) {
|
||||
_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];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const final = evt.RecognitionStatus === 'Success';
|
||||
@@ -471,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();
|
||||
@@ -488,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:
|
||||
@@ -496,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');
|
||||
@@ -508,7 +591,7 @@ class TaskGather extends Task {
|
||||
if (this.partialResultHook) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt},
|
||||
this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt},
|
||||
this.cs.callInfo, httpHeaders));
|
||||
}
|
||||
}
|
||||
@@ -519,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);
|
||||
}
|
||||
}
|
||||
@@ -532,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,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'));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class TaskHangup extends Task {
|
||||
/**
|
||||
* Hangup the call
|
||||
*/
|
||||
async exec(cs, dlg) {
|
||||
async exec(cs, {dlg}) {
|
||||
await super.exec(cs);
|
||||
try {
|
||||
await dlg.destroy({headers: this.headers});
|
||||
|
||||
@@ -8,7 +8,7 @@ class TaskLeave extends Task {
|
||||
|
||||
get name() { return TaskName.Leave; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class Lex extends Task {
|
||||
|
||||
get name() { return TaskName.Lex; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
|
||||
try {
|
||||
|
||||
@@ -26,7 +26,7 @@ class TaskListen extends Task {
|
||||
|
||||
get name() { return TaskName.Listen; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
this._dtmfHandler = this._onDtmf.bind(this, ep);
|
||||
@@ -40,7 +40,7 @@ class TaskListen extends Task {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`);
|
||||
this.transcribeTask.span = span;
|
||||
this.transcribeTask.ctx = ctx;
|
||||
this.transcribeTask.exec(cs, ep)
|
||||
this.transcribeTask.exec(cs, {ep})
|
||||
.then((result) => span.end())
|
||||
.catch((err) => span.end());
|
||||
}
|
||||
@@ -60,14 +60,21 @@ class TaskListen extends Task {
|
||||
this._clearTimer();
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.logger.debug('TaskListen:kill closing websocket');
|
||||
await this.ep.forkAudioStop()
|
||||
.catch((err) => this.logger.info(err, 'TaskListen:kill'));
|
||||
try {
|
||||
await this.ep.forkAudioStop();
|
||||
this.logger.debug('TaskListen:kill successfully closed websocket');
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'TaskListen:kill');
|
||||
}
|
||||
}
|
||||
if (this.recordStartTime) {
|
||||
const duration = moment().diff(this.recordStartTime, 'seconds');
|
||||
this.results.dialCallDuration = duration;
|
||||
}
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
if (this.transcribeTask) {
|
||||
await this.transcribeTask.kill(cs);
|
||||
this.transcribeTask = null;
|
||||
}
|
||||
this.ep && this._removeListeners(this.ep);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
@@ -212,7 +219,7 @@ class TaskListen extends Task {
|
||||
this.logger.debug('Listen:whisper tasks starting');
|
||||
while (tasks.length && !cs.callGone) {
|
||||
const task = tasks.shift();
|
||||
await task.exec(cs, this.ep);
|
||||
await task.exec(cs, {ep: this.ep});
|
||||
}
|
||||
this.logger.debug('Listen:whisper tasks complete');
|
||||
} catch (err) {
|
||||
|
||||
@@ -10,7 +10,7 @@ class TaskPause extends Task {
|
||||
|
||||
get name() { return TaskName.Pause; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
this.timer = setTimeout(this.notifyTaskDone.bind(this), this.length * 1000);
|
||||
await this.awaitTaskDone();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -17,16 +19,27 @@ class TaskPlay extends Task {
|
||||
return `${this.name}:{url=${this.url}}`;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
try {
|
||||
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}`);
|
||||
|
||||
@@ -20,7 +20,7 @@ class Rasa extends Task {
|
||||
return this.reportedFinalAction || this.isReplacingApplication;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
|
||||
this.ep = ep;
|
||||
@@ -34,7 +34,7 @@ class Rasa extends Task {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`);
|
||||
this.gatherTask.span = span;
|
||||
this.gatherTask.ctx = ctx;
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
this.gatherTask.exec(cs, {ep})
|
||||
.then(() => span.end())
|
||||
.catch((err) => {
|
||||
span.end();
|
||||
@@ -128,7 +128,7 @@ class Rasa extends Task {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`);
|
||||
this.gatherTask.span = span;
|
||||
this.gatherTask.ctx = ctx;
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
this.gatherTask.exec(cs, {ep})
|
||||
.then(() => span.end())
|
||||
.catch((err) => {
|
||||
span.end();
|
||||
|
||||
@@ -23,7 +23,7 @@ class TaskSayLegacy extends Task {
|
||||
|
||||
get name() { return TaskName.SayLegacy; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
super.exec(cs);
|
||||
this.ep = ep;
|
||||
try {
|
||||
|
||||
@@ -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 || {};
|
||||
@@ -22,7 +117,7 @@ class TaskSay extends Task {
|
||||
return `${this.name}{${this.text[0]}}`;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
|
||||
const {srf} = cs;
|
||||
|
||||
@@ -17,7 +17,7 @@ class TaskSipRequest extends Task {
|
||||
|
||||
get name() { return TaskName.SipRequest; }
|
||||
|
||||
async exec(cs, dlg) {
|
||||
async exec(cs, {dlg}) {
|
||||
super.exec(cs);
|
||||
try {
|
||||
this.logger.info({dlg}, `TaskSipRequest: sending a SIP ${this.method}`);
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
"synthesizer": "#synthesizer",
|
||||
"recognizer": "#recognizer",
|
||||
"bargeIn": "#bargeIn",
|
||||
"record": "#recordOptions"
|
||||
"record": "#recordOptions",
|
||||
"amd": "#amd"
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
@@ -83,7 +84,7 @@
|
||||
},
|
||||
"leave": {
|
||||
"properties": {
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
"hangup": {
|
||||
@@ -95,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"
|
||||
@@ -172,7 +176,8 @@
|
||||
"timeLimit": "number",
|
||||
"timeout": "number",
|
||||
"proxy": "string",
|
||||
"transcribe": "#transcribe"
|
||||
"transcribe": "#transcribe",
|
||||
"amd": "#amd"
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
@@ -492,6 +497,7 @@
|
||||
"requestSnr": "boolean",
|
||||
"initialSpeechTimeoutMs": "number",
|
||||
"azureServiceEndpoint": "string",
|
||||
"azureSttEndpointId": "string",
|
||||
"asrDtmfTerminationDigit": "string",
|
||||
"asrTimeout": "number"
|
||||
},
|
||||
@@ -512,10 +518,29 @@
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"voiceMs": "number",
|
||||
"mode": "number"
|
||||
"mode": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
"amd": {
|
||||
"properties": {
|
||||
"actionHook": "object|string",
|
||||
"thresholdWordCount": "number",
|
||||
"timers": "#amdTimers",
|
||||
"recognizer": "#recognizer"
|
||||
},
|
||||
"required": [
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"amdTimers": {
|
||||
"properties": {
|
||||
"noSpeechTimeoutMs": "number",
|
||||
"decisionTimeoutMs": "number",
|
||||
"toneTimeoutMs": "number",
|
||||
"greetingCompletionTimeoutMs": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,12 +171,13 @@ class Task extends Emitter {
|
||||
}
|
||||
|
||||
async performHook(cs, hook, results) {
|
||||
const params = results ? Object.assign(cs.callInfo.toJSON(), results) : cs.callInfo.toJSON();
|
||||
const span = this.startSpan('verb:hook', {'hook.url': hook});
|
||||
const b3 = this.getTracingPropagation('b3', span);
|
||||
const httpHeaders = b3 && {b3};
|
||||
span.setAttributes({'http.body': JSON.stringify(results)});
|
||||
span.setAttributes({'http.body': JSON.stringify(params)});
|
||||
try {
|
||||
const json = await cs.requestor.request('verb:hook', hook, results, httpHeaders);
|
||||
const json = await cs.requestor.request('verb:hook', hook, params, httpHeaders);
|
||||
span.setAttributes({'http.statusCode': 200});
|
||||
span.end();
|
||||
if (json && Array.isArray(json)) {
|
||||
@@ -335,6 +336,9 @@ class Task extends Emitter {
|
||||
}
|
||||
required = required.filter((item) => item !== dKey);
|
||||
}
|
||||
else if (dKey === '_') {
|
||||
/* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */
|
||||
}
|
||||
else throw new Error(`${name}: unknown property ${dKey}`);
|
||||
}
|
||||
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);
|
||||
|
||||
@@ -54,14 +54,31 @@ 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; }
|
||||
|
||||
async exec(cs, ep, ep2) {
|
||||
async exec(cs, {ep, ep2}) {
|
||||
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;
|
||||
@@ -80,7 +97,9 @@ class TaskTranscribe extends Task {
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
await this._startTranscribing(cs, ep, 1);
|
||||
if (this.separateRecognitionPerChannel && ep2) await this._startTranscribing(cs, ep2, 2);
|
||||
if (this.separateRecognitionPerChannel && ep2) {
|
||||
await this._startTranscribing(cs, ep2, 2);
|
||||
}
|
||||
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
@@ -102,18 +121,21 @@ class TaskTranscribe extends Task {
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected) {
|
||||
let stopTranscription = false;
|
||||
if (this.ep?.connected) {
|
||||
stopTranscription = true;
|
||||
this.ep.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||
|
||||
// hangup after 1 sec if we don't get a final transcription
|
||||
this._timer = setTimeout(() => this.notifyTaskDone(), 1000);
|
||||
}
|
||||
if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) {
|
||||
stopTranscription = true;
|
||||
this.ep2.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||
}
|
||||
// hangup after 1 sec if we don't get a final transcription
|
||||
if (stopTranscription) this._timer = setTimeout(() => this.notifyTaskDone(), 1500);
|
||||
else this.notifyTaskDone();
|
||||
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
|
||||
@@ -140,6 +162,7 @@ class TaskTranscribe extends Task {
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
||||
|
||||
if (this.vendor === 'google') {
|
||||
this.bugname = 'google_transcribe';
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
[
|
||||
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
|
||||
@@ -151,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;
|
||||
}
|
||||
@@ -175,6 +200,7 @@ class TaskTranscribe extends Task {
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
|
||||
}
|
||||
else if (this.vendor === 'aws') {
|
||||
this.bugname = 'aws_transcribe';
|
||||
[
|
||||
['diarization', 'AWS_SHOW_SPEAKER_LABEL'],
|
||||
['identifyChannels', 'AWS_ENABLE_CHANNEL_IDENTIFICATION']
|
||||
@@ -206,14 +232,23 @@ class TaskTranscribe extends Task {
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
|
||||
}
|
||||
else if (this.vendor === 'microsoft') {
|
||||
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;
|
||||
@@ -231,11 +266,16 @@ class TaskTranscribe extends Task {
|
||||
vendor: this.vendor,
|
||||
interim: this.interim ? true : false,
|
||||
locale: this.language,
|
||||
channels: /*this.separateRecognitionPerChannel ? 2 : */ 1
|
||||
channels: /*this.separateRecognitionPerChannel ? 2 : */ 1,
|
||||
bugname: this.bugname
|
||||
});
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, channel, evt) {
|
||||
_onTranscription(cs, ep, channel, evt, fsEvent) {
|
||||
// make sure this is not a transcript from answering machine detection
|
||||
const bugname = fsEvent.getHeader('media-bugname');
|
||||
if (bugname && this.bugname !== bugname) return;
|
||||
|
||||
this.logger.debug({evt, channel}, 'TaskTranscribe:_onTranscription');
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
@@ -249,7 +289,7 @@ class TaskTranscribe extends Task {
|
||||
}) :
|
||||
[
|
||||
{
|
||||
transcript: evt.DisplayText
|
||||
transcript: evt.DisplayText || evt.Text
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
324
lib/utils/amd-utils.js
Normal file
324
lib/utils/amd-utils.js
Normal file
@@ -0,0 +1,324 @@
|
||||
const Emitter = require('events');
|
||||
const {readFile} = require('fs');
|
||||
const {
|
||||
GoogleTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
AmdEvents,
|
||||
AvmdEvents
|
||||
} = require('./constants');
|
||||
const bugname = 'amd_bug';
|
||||
const {VMD_HINTS_FILE} = process.env;
|
||||
let voicemailHints = [];
|
||||
|
||||
const updateHints = async(file, callback) => {
|
||||
readFile(file, 'utf8', (err, data) => {
|
||||
if (err) return callback(err);
|
||||
try {
|
||||
callback(null, JSON.parse(data));
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (VMD_HINTS_FILE) {
|
||||
updateHints(VMD_HINTS_FILE, (err, hints) => {
|
||||
if (err) { console.error(err); }
|
||||
voicemailHints = hints;
|
||||
|
||||
/* if successful, update the hints every hour */
|
||||
setInterval(() => {
|
||||
updateHints(VMD_HINTS_FILE, (err, hints) => {
|
||||
if (err) { console.error(err); }
|
||||
voicemailHints = hints;
|
||||
});
|
||||
}, 60000);
|
||||
});
|
||||
}
|
||||
|
||||
class Amd extends Emitter {
|
||||
constructor(logger, cs, opts) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.vendor = opts.recognizer?.vendor || cs.speechRecognizerVendor;
|
||||
if ('default' === this.vendor) this.vendor = cs.speechRecognizerVendor;
|
||||
|
||||
this.language = opts.recognizer?.language || cs.speechRecognizerLanguage;
|
||||
if ('default' === this.language) this.language = cs.speechRecognizerLanguage;
|
||||
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
|
||||
if (!this.sttCredentials) throw new Error(`No speech credentials found for vendor ${this.vendor}`);
|
||||
|
||||
this.thresholdWordCount = opts.thresholdWordCount || 9;
|
||||
const {normalizeTranscription} = require('./transcription-utils')(logger);
|
||||
this.normalizeTranscription = normalizeTranscription;
|
||||
|
||||
const {
|
||||
noSpeechTimeoutMs = 5000,
|
||||
decisionTimeoutMs = 15000,
|
||||
toneTimeoutMs = 20000,
|
||||
greetingCompletionTimeoutMs = 2000
|
||||
} = opts.timers || {};
|
||||
this.noSpeechTimeoutMs = noSpeechTimeoutMs;
|
||||
this.decisionTimeoutMs = decisionTimeoutMs;
|
||||
this.toneTimeoutMs = toneTimeoutMs;
|
||||
this.greetingCompletionTimeoutMs = greetingCompletionTimeoutMs;
|
||||
|
||||
this.beepDetected = false;
|
||||
}
|
||||
|
||||
startDecisionTimer() {
|
||||
this.decisionTimer = setTimeout(this._onDecisionTimeout.bind(this), this.decisionTimeoutMs);
|
||||
this.noSpeechTimer = setTimeout(this._onNoSpeechTimeout.bind(this), this.noSpeechTimeoutMs);
|
||||
this.startToneTimer();
|
||||
}
|
||||
stopDecisionTimer() {
|
||||
this.decisionTimer && clearTimeout(this.decisionTimer);
|
||||
}
|
||||
stopNoSpeechTimer() {
|
||||
this.noSpeechTimer && clearTimeout(this.noSpeechTimer);
|
||||
}
|
||||
startToneTimer() {
|
||||
this.toneTimer = setTimeout(this._onToneTimeout.bind(this), this.toneTimeoutMs);
|
||||
}
|
||||
startGreetingCompletionTimer() {
|
||||
this.greetingCompletionTimer = setTimeout(
|
||||
this._onGreetingCompletionTimeout.bind(this),
|
||||
this.beepDetected ? 1000 : this.greetingCompletionTimeoutMs);
|
||||
}
|
||||
stopGreetingCompletionTimer() {
|
||||
this.greetingCompletionTimer && clearTimeout(this.greetingCompletionTimer);
|
||||
}
|
||||
restartGreetingCompletionTimer() {
|
||||
this.stopGreetingCompletionTimer();
|
||||
this.startGreetingCompletionTimer();
|
||||
}
|
||||
stopToneTimer() {
|
||||
this.toneTimer && clearTimeout(this.toneTimer);
|
||||
}
|
||||
stopAllTimers() {
|
||||
this.stopDecisionTimer();
|
||||
this.stopNoSpeechTimer();
|
||||
this.stopToneTimer();
|
||||
this.stopGreetingCompletionTimer();
|
||||
}
|
||||
_onDecisionTimeout() {
|
||||
this.emit(this.decision = AmdEvents.DecisionTimeout);
|
||||
this.stopNoSpeechTimer();
|
||||
}
|
||||
_onToneTimeout() {
|
||||
this.emit(AmdEvents.ToneTimeout);
|
||||
}
|
||||
_onNoSpeechTimeout() {
|
||||
this.emit(this.decision = AmdEvents.NoSpeechDetected);
|
||||
this.stopDecisionTimer();
|
||||
}
|
||||
_onGreetingCompletionTimeout() {
|
||||
this.emit(AmdEvents.MachineStoppedSpeaking);
|
||||
}
|
||||
|
||||
evaluateTranscription(evt) {
|
||||
if (this.decision) {
|
||||
/* at this point we are only listening for the machine to stop speaking */
|
||||
if (this.decision === AmdEvents.MachineDetected) {
|
||||
this.restartGreetingCompletionTimer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.stopNoSpeechTimer();
|
||||
|
||||
this.logger.debug({evt}, 'Amd:evaluateTranscription - raw');
|
||||
const t = this.normalizeTranscription(evt, this.vendor, this.language);
|
||||
const hints = voicemailHints[this.language] || [];
|
||||
|
||||
this.logger.debug({t}, 'Amd:evaluateTranscription - normalized');
|
||||
|
||||
if (Array.isArray(t.alternatives) && t.alternatives.length > 0) {
|
||||
const wordCount = t.alternatives[0].transcript.split(' ').length;
|
||||
const final = t.is_final;
|
||||
|
||||
const foundHint = hints.find((h) => t.alternatives[0].transcript.includes(h));
|
||||
if (foundHint) {
|
||||
/* we detected a common voice mail greeting */
|
||||
this.logger.debug(`Amd:evaluateTranscription: found hint ${foundHint}`);
|
||||
this.emit(this.decision = AmdEvents.MachineDetected, {
|
||||
reason: 'hint',
|
||||
hint: foundHint,
|
||||
language: t.language_code
|
||||
});
|
||||
}
|
||||
else if (final && wordCount < this.thresholdWordCount) {
|
||||
/* a short greeting is typically a human */
|
||||
this.emit(this.decision = AmdEvents.HumanDetected, {
|
||||
reason: 'short greeting',
|
||||
greeting: t.alternatives[0].transcript,
|
||||
language: t.language_code
|
||||
});
|
||||
}
|
||||
else if (wordCount >= this.thresholdWordCount) {
|
||||
/* a long greeting is typically a machine */
|
||||
this.emit(this.decision = AmdEvents.MachineDetected, {
|
||||
reason: 'long greeting',
|
||||
greeting: t.alternatives[0].transcript,
|
||||
language: t.language_code
|
||||
});
|
||||
}
|
||||
|
||||
if (this.decision) {
|
||||
this.stopDecisionTimer();
|
||||
|
||||
if (this.decision === AmdEvents.MachineDetected) {
|
||||
/* if we detected a machine, then wait for greeting to end */
|
||||
this.startGreetingCompletionTimer();
|
||||
}
|
||||
}
|
||||
return this.decision;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (logger) => {
|
||||
const startTranscribing = async(cs, ep, task) => {
|
||||
const {vendor, language} = ep.amd;
|
||||
ep.startTranscription({
|
||||
vendor,
|
||||
language,
|
||||
interim: true,
|
||||
bugname
|
||||
}).catch((err) => {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
ep.amd = null;
|
||||
task.emit(AmdEvents.Error, err);
|
||||
logger.error(err, 'amd:_startTranscribing error');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
vendor: vendor,
|
||||
detail: err.message
|
||||
});
|
||||
}).catch((err) => logger.info({err}, 'Error generating alert for tts failure'));
|
||||
|
||||
};
|
||||
|
||||
const onEndOfUtterance = (cs, ep, task) => {
|
||||
logger.debug('amd:onEndOfUtterance');
|
||||
startTranscribing(cs, ep, task);
|
||||
};
|
||||
const onNoSpeechDetected = (cs, ep, task) => {
|
||||
logger.debug('amd:onNoSpeechDetected');
|
||||
ep.amd.stopAllTimers();
|
||||
task.emit(AmdEvents.NoSpeechDetected);
|
||||
};
|
||||
const onTranscription = (cs, ep, task, evt, fsEvent) => {
|
||||
if (fsEvent.getHeader('media-bugname') !== bugname) return;
|
||||
ep.amd?.evaluateTranscription(evt);
|
||||
};
|
||||
const onBeep = (cs, ep, task, evt, fsEvent) => {
|
||||
logger.debug({evt, fsEvent}, 'onBeep');
|
||||
const frequency = Math.floor(fsEvent.getHeader('Frequency'));
|
||||
const variance = Math.floor(fsEvent.getHeader('Frequency-variance'));
|
||||
task.emit('amd', {type: AmdEvents.ToneDetected, frequency, variance});
|
||||
if (ep.amd) {
|
||||
ep.amd.stopToneTimer();
|
||||
ep.amd.beepDetected = true;
|
||||
}
|
||||
ep.execute('avmd_stop').catch((err) => this.logger.info(err, 'Error stopping avmd'));
|
||||
};
|
||||
|
||||
const startAmd = async(cs, ep, task, opts) => {
|
||||
const amd = ep.amd = new Amd(logger, cs, opts);
|
||||
const {vendor, language, sttCredentials} = amd;
|
||||
const sttOpts = {};
|
||||
const hints = voicemailHints[language] || [];
|
||||
|
||||
/* set stt options */
|
||||
logger.info(`starting amd for vendor ${vendor} and language ${language}`);
|
||||
if ('google' === vendor) {
|
||||
sttOpts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(sttCredentials.credentials);
|
||||
sttOpts.GOOGLE_SPEECH_USE_ENHANCED = true;
|
||||
sttOpts.GOOGLE_SPEECH_HINTS = hints.join(',');
|
||||
if (opts.recognizer?.altLanguages) {
|
||||
sttOpts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
|
||||
}
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, onEndOfUtterance.bind(null, cs, ep, task));
|
||||
}
|
||||
else if (['aws', 'polly'].includes(vendor)) {
|
||||
Object.assign(sttOpts, {
|
||||
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
||||
AWS_REGION: sttCredentials.region
|
||||
});
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
||||
}
|
||||
else if ('microsoft' === vendor) {
|
||||
Object.assign(sttOpts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': sttCredentials.api_key,
|
||||
'AZURE_REGION': sttCredentials.region
|
||||
});
|
||||
sttOpts.AZURE_SPEECH_HINTS = hints.join(',');
|
||||
if (opts.recognizer?.altLanguages) {
|
||||
sttOpts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
|
||||
}
|
||||
sttOpts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = opts.resolveTimeoutMs || 20000;
|
||||
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
|
||||
}
|
||||
logger.debug({sttOpts}, 'startAmd: setting channel vars');
|
||||
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
|
||||
|
||||
amd
|
||||
.on(AmdEvents.NoSpeechDetected, (evt) => {
|
||||
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
|
||||
ep.stopTranscription({vendor, bugname});
|
||||
})
|
||||
.on(AmdEvents.HumanDetected, (evt) => {
|
||||
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
|
||||
ep.stopTranscription({vendor, bugname});
|
||||
})
|
||||
.on(AmdEvents.MachineDetected, (evt) => {
|
||||
task.emit('amd', {type: AmdEvents.MachineDetected, ...evt});
|
||||
})
|
||||
.on(AmdEvents.DecisionTimeout, (evt) => {
|
||||
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
|
||||
ep.stopTranscription({vendor, bugname});
|
||||
})
|
||||
.on(AmdEvents.ToneTimeout, (evt) => {
|
||||
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
|
||||
ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
|
||||
})
|
||||
.on(AmdEvents.MachineStoppedSpeaking, () => {
|
||||
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
|
||||
ep.stopTranscription({vendor, bugname});
|
||||
});
|
||||
|
||||
/* start transcribing, and also listening for beep */
|
||||
amd.startDecisionTimer();
|
||||
startTranscribing(cs, ep, task);
|
||||
|
||||
ep.addCustomEventListener(AvmdEvents.Beep, onBeep.bind(null, cs, ep, task));
|
||||
ep.execute('avmd_start').catch((err) => this.logger.info(err, 'Error starting avmd'));
|
||||
};
|
||||
|
||||
const stopAmd = (ep, task) => {
|
||||
let vendor;
|
||||
if (ep.amd) {
|
||||
vendor = ep.amd.vendor;
|
||||
ep.amd.stopAllTimers();
|
||||
ep.amd = null;
|
||||
}
|
||||
|
||||
if (ep.connected) {
|
||||
ep.stopTranscription({vendor, bugname})
|
||||
.catch((err) => logger.info(err, 'stopAmd: Error stopping transcription'));
|
||||
task.emit('amd', {type: AmdEvents.Stopped});
|
||||
ep.execute('avmd_stop').catch((err) => this.logger.info(err, 'Error stopping avmd'));
|
||||
}
|
||||
ep.removeCustomEventListener(AvmdEvents.Beep);
|
||||
};
|
||||
|
||||
return {startAmd, stopAmd};
|
||||
};
|
||||
@@ -29,6 +29,7 @@
|
||||
"Tag": "tag",
|
||||
"Transcribe": "transcribe"
|
||||
},
|
||||
"AllowedSipRecVerbs": ["config", "gather", "transcribe", "listen"],
|
||||
"CallStatus": {
|
||||
"Trying": "trying",
|
||||
"Ringing": "ringing",
|
||||
@@ -56,6 +57,9 @@
|
||||
"StableCall": "stable-call",
|
||||
"UnansweredCall": "unanswered-call"
|
||||
},
|
||||
"AvmdEvents": {
|
||||
"Beep": "avmd::beep"
|
||||
},
|
||||
"GoogleTranscriptionEvents": {
|
||||
"Transcription": "google_transcribe::transcription",
|
||||
"EndOfUtterance": "google_transcribe::end_of_utterance",
|
||||
@@ -125,6 +129,17 @@
|
||||
"RecordingOff": "recording_off",
|
||||
"RecordingPaused": "recording_paused"
|
||||
},
|
||||
"AmdEvents": {
|
||||
"NoSpeechDetected": "amd_no_speech_detected",
|
||||
"HumanDetected": "amd_human_detected",
|
||||
"MachineDetected": "amd_machine_detected",
|
||||
"MachineStoppedSpeaking": "amd_machine_stopped_speaking",
|
||||
"Error": "amd_error",
|
||||
"DecisionTimeout": "amd_decision_timeout",
|
||||
"ToneDetected": "amd_tone_detected",
|
||||
"ToneTimeout": "amd_tone_timeout",
|
||||
"Stopped": "amd_stopped"
|
||||
},
|
||||
"MAX_SIMRINGS": 10,
|
||||
"BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)",
|
||||
"FS_UUID_SET_NAME": "fsUUIDs"
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -30,11 +30,14 @@ const speechMapper = (cred) => {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.access_key_id = o.access_key_id;
|
||||
obj.secret_access_key = o.secret_access_key;
|
||||
obj.aws_region = o.aws_region;
|
||||
}
|
||||
else if ('microsoft' === obj.vendor) {
|
||||
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));
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const bent = require('bent');
|
||||
const {Client, Pool} = require('undici');
|
||||
const parseUrl = require('parse-url');
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const pools = new Map();
|
||||
const HTTP_TIMEOUT = 10000;
|
||||
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
|
||||
@@ -22,21 +24,45 @@ class HttpRequestor extends BaseRequestor {
|
||||
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}`;
|
||||
this.get = bent(baseUrl, 'GET', 'buffer', 200, 201);
|
||||
this.post = bent(baseUrl, 'POST', 'buffer', 200, 201);
|
||||
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
assert(['GET', 'POST'].includes(this.method));
|
||||
|
||||
const u = this._parsedUrl = parseUrl(this.url);
|
||||
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);
|
||||
|
||||
if (this._usePools) {
|
||||
if (pools.has(this._baseUrl)) {
|
||||
this.client = pools.get(this._baseUrl);
|
||||
}
|
||||
else {
|
||||
const connections = process.env.HTTP_POOLSIZE ? parseInt(process.env.HTTP_POOLSIZE) : 10;
|
||||
const pipelining = process.env.HTTP_PIPELINING ? parseInt(process.env.HTTP_PIPELINING) : 1;
|
||||
const pool = this.client = new Pool(this._baseUrl, {
|
||||
connections,
|
||||
pipelining
|
||||
});
|
||||
pools.set(this._baseUrl, pool);
|
||||
this.logger.debug(`HttpRequestor:created pool for ${this._baseUrl}`);
|
||||
}
|
||||
}
|
||||
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() {
|
||||
return this._baseUrl;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this._usePools && !this.client?.closed) this.client.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request.
|
||||
* All requests use json bodies.
|
||||
@@ -57,21 +83,60 @@ class HttpRequestor extends BaseRequestor {
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
const url = hook.url || hook;
|
||||
const method = hook.method || 'POST';
|
||||
let buf = '';
|
||||
|
||||
assert.ok(url, 'HttpRequestor:request url was not provided');
|
||||
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor: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}, `HttpRequestor:request ${method} ${url}`);
|
||||
const startAt = process.hrtime();
|
||||
|
||||
let buf;
|
||||
let newClient;
|
||||
try {
|
||||
let client, path, query;
|
||||
if (this._isRelativeUrl(url)) {
|
||||
client = this.client;
|
||||
path = url;
|
||||
}
|
||||
else {
|
||||
const u = parseUrl(url);
|
||||
if (u.resource === this._resource && u.protocol === this._protocol) {
|
||||
client = this.client;
|
||||
path = u.pathname;
|
||||
query = u.query;
|
||||
}
|
||||
else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
const sigHeader = this._generateSigHeader(payload, this.secret);
|
||||
const headers = {...sigHeader, ...this.authHeader, ...httpHeaders};
|
||||
this.logger.debug({url, headers}, 'send webhook');
|
||||
buf = this._isRelativeUrl(url) ?
|
||||
await this.post(url, payload, headers) :
|
||||
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
|
||||
const hdrs = {
|
||||
...sigHeader,
|
||||
...this.authHeader,
|
||||
...httpHeaders,
|
||||
...('POST' === method && {'Content-Type': 'application/json'})
|
||||
};
|
||||
const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url;
|
||||
this.logger.debug({url, absUrl, hdrs}, 'send webhook');
|
||||
const {statusCode, headers, body} = await client.request({
|
||||
path,
|
||||
query,
|
||||
method,
|
||||
headers: hdrs,
|
||||
...('POST' === method && {body: JSON.stringify(payload)}),
|
||||
timeout: HTTP_TIMEOUT,
|
||||
followRedirects: false
|
||||
});
|
||||
if (![200, 202, 204].includes(statusCode)) {
|
||||
const err = new Error();
|
||||
err.statusCode = statusCode;
|
||||
throw err;
|
||||
}
|
||||
if (headers['content-type']?.includes('application/json')) {
|
||||
buf = await body.json();
|
||||
}
|
||||
if (newClient) newClient.close();
|
||||
} catch (err) {
|
||||
if (err.statusCode) {
|
||||
this.logger.info({baseUrl: this.baseUrl, url},
|
||||
@@ -93,20 +158,15 @@ class HttpRequestor extends BaseRequestor {
|
||||
}
|
||||
this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
|
||||
|
||||
if (newClient) newClient.close();
|
||||
throw err;
|
||||
}
|
||||
const rtt = this._roundTrip(startAt);
|
||||
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}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
|
||||
return json;
|
||||
}
|
||||
catch (err) {
|
||||
//this.logger.debug({err, url, method}, `HttpRequestor:request returned non-JSON content: '${buf.toString()}'`);
|
||||
}
|
||||
if (buf && Array.isArray(buf)) {
|
||||
this.logger.info({response: buf}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
248
lib/utils/siprec-utils.js
Normal file
248
lib/utils/siprec-utils.js
Normal file
@@ -0,0 +1,248 @@
|
||||
const xmlParser = require('xml2js').parseString;
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const transform = require('sdp-transform');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
|
||||
const parseCallData = (prefix, obj) => {
|
||||
const ret = {};
|
||||
const group = obj[`${prefix}group`];
|
||||
if (group) {
|
||||
const key = Object.keys(group[0]).find((k) => /:?callData$/.test(k));
|
||||
//const o = _.find(group[0], (value, key) => /:?callData$/.test(key));
|
||||
if (key) {
|
||||
//const callData = o[0];
|
||||
const callData = group[0][key];
|
||||
for (const key of Object.keys(callData)) {
|
||||
if (['fromhdr', 'tohdr', 'callid'].includes(key)) ret[key] = callData[key][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
debug('parseCallData', prefix, obj, ret);
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* parse a SIPREC multiparty body
|
||||
* @param {object} opts - options
|
||||
* @return {Promise}
|
||||
*/
|
||||
const parseSiprecPayload = (req, logger) => {
|
||||
const opts = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
let sdp, meta ;
|
||||
for (let i = 0; i < req.payload.length; i++) {
|
||||
switch (req.payload[i].type) {
|
||||
case 'application/sdp':
|
||||
sdp = req.payload[i].content ;
|
||||
break ;
|
||||
|
||||
case 'application/rs-metadata+xml':
|
||||
case 'application/rs-metadata':
|
||||
meta = opts.xml = req.payload[i].content ;
|
||||
break ;
|
||||
|
||||
default:
|
||||
break ;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sdp || !meta) {
|
||||
logger.info({payload: req.payload}, 'invalid SIPREC payload');
|
||||
return reject(new Error('expected multipart SIPREC body'));
|
||||
}
|
||||
|
||||
xmlParser(meta, (err, result) => {
|
||||
if (err) { throw err; }
|
||||
|
||||
opts.recordingData = result ;
|
||||
opts.sessionId = uuidv4() ;
|
||||
|
||||
const arr = /^([^]+)(m=[^]+?)(m=[^]+?)$/.exec(sdp) ;
|
||||
opts.sdp1 = `${arr[1]}${arr[2]}` ;
|
||||
opts.sdp2 = `${arr[1]}${arr[3]}\r\n` ;
|
||||
|
||||
try {
|
||||
if (typeof result === 'object' && Object.keys(result).length === 1) {
|
||||
const key = Object.keys(result)[0] ;
|
||||
const arr = /^(.*:)recording/.exec(key) ;
|
||||
const prefix = !arr ? '' : (arr[1]) ;
|
||||
const obj = opts.recordingData[`${prefix}recording`];
|
||||
|
||||
// 1. collect participant data
|
||||
const participants = {} ;
|
||||
obj[`${prefix}participant`].forEach((p) => {
|
||||
const partDetails = {} ;
|
||||
participants[p.$.participant_id] = partDetails;
|
||||
if ((`${prefix}nameID` in p) && Array.isArray(p[`${prefix}nameID`])) {
|
||||
partDetails.aor = p[`${prefix}nameID`][0].$.aor;
|
||||
if ('name' in p[`${prefix}nameID`][0] && Array.isArray(p[`${prefix}nameID`][0].name)) {
|
||||
const name = p[`${prefix}nameID`][0].name[0];
|
||||
if (typeof name === 'string') partDetails.name = name ;
|
||||
else if (typeof name === 'object') partDetails.name = name._ ;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. find the associated streams for each participant
|
||||
if (`${prefix}participantstreamassoc` in obj) {
|
||||
obj[`${prefix}participantstreamassoc`].forEach((ps) => {
|
||||
const part = participants[ps.$.participant_id];
|
||||
if (part) {
|
||||
part.send = ps[`${prefix}send`][0];
|
||||
part.recv = ps[`${prefix}recv`][0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Retrieve stream data
|
||||
opts.caller = {} ;
|
||||
opts.callee = {} ;
|
||||
obj[`${prefix}stream`].forEach((s) => {
|
||||
const streamId = s.$.stream_id;
|
||||
let sender;
|
||||
for (const [k, v] of Object.entries(participants)) {
|
||||
if (v.send === streamId) {
|
||||
sender = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//const sender = _.find(participants, { 'send': streamId});
|
||||
|
||||
if (!sender) return;
|
||||
|
||||
sender.label = s[`${prefix}label`][0];
|
||||
|
||||
if (-1 !== ['1', 'a_leg', 'inbound'].indexOf(sender.label)) {
|
||||
opts.caller.aor = sender.aor ;
|
||||
if (sender.name) opts.caller.name = sender.name;
|
||||
}
|
||||
else {
|
||||
opts.callee.aor = sender.aor ;
|
||||
if (sender.name) opts.callee.name = sender.name;
|
||||
}
|
||||
});
|
||||
|
||||
// if we dont have a participantstreamassoc then assume the first participant is the caller
|
||||
if (!opts.caller.aor && !opts.callee.aor) {
|
||||
let i = 0;
|
||||
for (const part in participants) {
|
||||
const p = participants[part];
|
||||
if (0 === i && p.aor) {
|
||||
opts.caller.aor = p.aor;
|
||||
opts.caller.name = p.name;
|
||||
}
|
||||
else if (1 === i && p.aor) {
|
||||
opts.callee.aor = p.aor;
|
||||
opts.callee.name = p.name;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// now for Sonus (at least) we get the original from, to and call-id headers in a <callData/> element
|
||||
// if so, this should take preference
|
||||
const callData = parseCallData(prefix, obj);
|
||||
if (callData) {
|
||||
debug(`callData: ${JSON.stringify(callData)}`);
|
||||
opts.originalCallId = callData.callid;
|
||||
|
||||
// caller
|
||||
let r1 = /^(.*)(<sip.*)$/.exec(callData.fromhdr);
|
||||
if (r1) {
|
||||
const arr = /<(.*)>/.exec(r1[2]);
|
||||
if (arr) {
|
||||
const uri = parseUri(arr[1]);
|
||||
const user = uri.user || 'anonymous';
|
||||
opts.caller.aor = `sip:${user}@${uri.host}`;
|
||||
}
|
||||
const dname = r1[1].trim();
|
||||
const arr2 = /"(.*)"/.exec(dname);
|
||||
if (arr2) opts.caller.name = arr2[1];
|
||||
else opts.caller.name = dname;
|
||||
}
|
||||
// callee
|
||||
r1 = /^(.*)(<sip.*)$/.exec(callData.tohdr);
|
||||
if (r1) {
|
||||
const arr = /<(.*)>/.exec(r1[2]);
|
||||
if (arr) {
|
||||
const uri = parseUri(arr[1]);
|
||||
opts.callee.aor = `sip:${uri.user}@${uri.host}`;
|
||||
}
|
||||
const dname = r1[1].trim();
|
||||
const arr2 = /"(.*)"/.exec(dname);
|
||||
if (arr2) opts.callee.name = arr2[1];
|
||||
else opts.callee.name = dname;
|
||||
}
|
||||
debug(`opts.caller from callData: ${JSON.stringify(opts.caller)}`);
|
||||
debug(`opts.callee from callData: ${JSON.stringify(opts.callee)}`);
|
||||
}
|
||||
|
||||
if (opts.caller.aor && 0 !== opts.caller.aor.indexOf('sip:')) {
|
||||
opts.caller.aor = 'sip:' + opts.caller.aor;
|
||||
}
|
||||
if (opts.callee.aor && 0 !== opts.callee.aor.indexOf('sip:')) {
|
||||
opts.callee.aor = 'sip:' + opts.callee.aor;
|
||||
}
|
||||
|
||||
if (opts.caller.aor) {
|
||||
const uri = parseUri(opts.caller.aor);
|
||||
opts.caller.number = uri.user;
|
||||
}
|
||||
if (opts.callee.aor) {
|
||||
const uri = parseUri(opts.callee.aor);
|
||||
opts.callee.number = uri.user;
|
||||
}
|
||||
opts.recordingSessionId = opts.recordingData[`${prefix}recording`][`${prefix}session`][0].$.session_id;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
debug(opts, 'payload parser results');
|
||||
resolve(opts) ;
|
||||
}) ;
|
||||
}) ;
|
||||
};
|
||||
|
||||
const createSipRecPayload = (sdp1, sdp2, logger) => {
|
||||
const sdpObj = [];
|
||||
sdpObj.push(transform.parse(sdp1));
|
||||
sdpObj.push(transform.parse(sdp2));
|
||||
|
||||
//const arr1 = /^([^]+)(c=[^]+)t=[^]+(m=[^]+?)(a=[^]+)$/.exec(sdp1) ;
|
||||
//const arr2 = /^([^]+)(c=[^]+)t=[^]+(m=[^]+?)(a=[^]+)$/.exec(sdp2) ;
|
||||
|
||||
debug(`sdp1: ${sdp1}`);
|
||||
debug(`objSdp[0]: ${JSON.stringify(sdpObj[0])}`);
|
||||
debug(`sdp2: ${sdp2}`);
|
||||
debug(`objSdp[1]: ${JSON.stringify(sdpObj[1])}`);
|
||||
|
||||
if (!sdpObj[0] || !sdpObj[0].media.length) {
|
||||
throw new Error(`Error parsing sdp1 into component parts: ${sdp1}`);
|
||||
}
|
||||
else if (!sdpObj[1] || !sdpObj[1].media.length) {
|
||||
throw new Error(`Error parsing sdp2 into component parts: ${sdp2}`);
|
||||
}
|
||||
|
||||
if (!sdpObj[0].media[0].label) sdpObj[0].media[0].label = 1;
|
||||
if (!sdpObj[1].media[0].label) sdpObj[1].media[0].label = 2;
|
||||
|
||||
//const aLabel = sdp1.includes('a=label:') ? '' : 'a=label:1\r\n';
|
||||
//const bLabel = sdp2.includes('a=label:') ? '' : 'a=label:2\r\n';
|
||||
|
||||
sdpObj[0].media = sdpObj[0].media.concat(sdpObj[1].media);
|
||||
const combinedSdp = transform.write(sdpObj[0])
|
||||
.replace(/a=sendonly\r\n/g, '')
|
||||
.replace(/a=direction:both\r\n/g, '');
|
||||
|
||||
debug(`combined ${combinedSdp}`);
|
||||
/*
|
||||
const combinedSdp = `${arr1[1]}t=0 0\r\n${arr1[2]}${arr1[3]}${arr1[4]}${aLabel}${arr2[3]}${arr2[4]}${bLabel}`
|
||||
.replace(/a=sendonly\r\n/g, '')
|
||||
.replace(/a=direction:both\r\n/g, '');
|
||||
*/
|
||||
return combinedSdp;
|
||||
};
|
||||
|
||||
module.exports = { parseSiprecPayload, createSipRecPayload } ;
|
||||
33
lib/utils/transcription-utils.js
Normal file
33
lib/utils/transcription-utils.js
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = (logger) => {
|
||||
const normalizeTranscription = (evt, vendor, channel) => {
|
||||
if ('aws' === vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const language_code = evt.PrimaryLanguage?.Language || this.language;
|
||||
const alternatives = nbest ? nbest.map((n) => {
|
||||
return {
|
||||
confidence: n.Confidence,
|
||||
transcript: n.Display
|
||||
};
|
||||
}) :
|
||||
[
|
||||
{
|
||||
transcript: evt.DisplayText || evt.Text
|
||||
}
|
||||
];
|
||||
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
channel,
|
||||
language_code,
|
||||
alternatives
|
||||
};
|
||||
evt = newEvent;
|
||||
}
|
||||
evt.channel_tag = channel;
|
||||
//logger.debug({evt}, 'normalized transcription');
|
||||
return evt;
|
||||
};
|
||||
|
||||
return {normalizeTranscription};
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
@@ -158,7 +158,7 @@ class WsRequestor extends BaseRequestor {
|
||||
|
||||
close() {
|
||||
this.closedGracefully = true;
|
||||
this.logger.info('WsRequestor:close closing socket');
|
||||
this.logger.debug('WsRequestor:close closing socket');
|
||||
try {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
150
package-lock.json
generated
150
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.5",
|
||||
"version": "v0.7.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.5",
|
||||
"version": "v0.7.7",
|
||||
"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.5",
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.3.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
|
||||
@@ -27,19 +27,20 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.1",
|
||||
"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.4",
|
||||
"parse-url": "^7.0.2",
|
||||
"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.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"
|
||||
},
|
||||
@@ -542,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",
|
||||
@@ -565,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.5",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.5.tgz",
|
||||
"integrity": "sha512-WrVenxY4ZgW0YYNIXdAz3ZKao8IQy612RVDAnYq7oWvHHqzgLmLrbIvTXKhycd27iYvwFAPCcUDk1EMuUoPLPA==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"influx": "^5.8.0"
|
||||
"influx": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
@@ -1991,9 +1992,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-fsmrf": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.1.tgz",
|
||||
"integrity": "sha512-W+HFsQaAyJrZuKQw0CuKGjvx5Ms0c7tCYt+7QbXprtXWwP80kX9gSyyY7n28VEzFeOi9+SIsYmVBXd+25g2hdg==",
|
||||
"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",
|
||||
@@ -3613,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",
|
||||
@@ -4435,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",
|
||||
@@ -4671,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": {
|
||||
@@ -5881,6 +5860,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/unix-dgram": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz",
|
||||
@@ -5984,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": {
|
||||
@@ -6682,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",
|
||||
@@ -6705,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.5",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.5.tgz",
|
||||
"integrity": "sha512-WrVenxY4ZgW0YYNIXdAz3ZKao8IQy612RVDAnYq7oWvHHqzgLmLrbIvTXKhycd27iYvwFAPCcUDk1EMuUoPLPA==",
|
||||
"requires": {
|
||||
"debug": "^4.3.1",
|
||||
"influx": "^5.8.0"
|
||||
"influx": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
@@ -7782,9 +7769,9 @@
|
||||
}
|
||||
},
|
||||
"drachtio-fsmrf": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.1.tgz",
|
||||
"integrity": "sha512-W+HFsQaAyJrZuKQw0CuKGjvx5Ms0c7tCYt+7QbXprtXWwP80kX9gSyyY7n28VEzFeOi9+SIsYmVBXd+25g2hdg==",
|
||||
"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",
|
||||
@@ -9021,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",
|
||||
@@ -9656,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",
|
||||
@@ -9829,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": {
|
||||
@@ -10773,6 +10744,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"undici": {
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz",
|
||||
@@ -10855,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": {
|
||||
|
||||
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.5",
|
||||
"version": "v0.7.7",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -16,21 +16,19 @@
|
||||
"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 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=info 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/ ",
|
||||
"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/ ",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"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.5",
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.3.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
|
||||
@@ -44,19 +42,20 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.1",
|
||||
"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.4",
|
||||
"parse-url": "^7.0.2",
|
||||
"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.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"
|
||||
},
|
||||
|
||||
108
test/create-call-test.js
Normal file
108
test/create-call-test.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const test = require('tape');
|
||||
const { sippUac } = require('./sipp')('test_fs');
|
||||
const bent = require('bent');
|
||||
const clearModule = require('clear-module');
|
||||
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);
|
||||
});
|
||||
|
||||
function connect(connectable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connectable.on('connect', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('test create-call timeout', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
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);
|
||||
post('v1/createCall', {
|
||||
'account_sid':account_sid,
|
||||
'timeout': 1,
|
||||
"call_hook": {
|
||||
"url": "https://public-apps.jambonz.us/hello-world",
|
||||
"method": "POST"
|
||||
},
|
||||
"from": "15083718299",
|
||||
"to": {
|
||||
"type": "phone",
|
||||
"number": "15583084809"
|
||||
}});
|
||||
//THEN
|
||||
await p;
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('test create-call call-hook basic authentication', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
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,
|
||||
"call_hook": {
|
||||
"url": "http://127.0.0.1:3100/",
|
||||
"method": "POST",
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
"from": from,
|
||||
"to": {
|
||||
"type": "phone",
|
||||
"number": "15583084809"
|
||||
}});
|
||||
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "say",
|
||||
"text": "hello"
|
||||
}
|
||||
];
|
||||
provisionCallHook(from, verbs);
|
||||
//THEN
|
||||
await p;
|
||||
|
||||
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}`)
|
||||
t.ok(obj.headers.Authorization = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||
'create-call: call-hook contains basic authentication header');
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
platform: linux/x86_64
|
||||
ports:
|
||||
- "3360:3306"
|
||||
environment:
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"]
|
||||
@@ -75,10 +75,8 @@ services:
|
||||
fs:
|
||||
ipv4_address: 172.38.0.51
|
||||
|
||||
webhook-decline:
|
||||
webhook-scaffold:
|
||||
image: jambonz/webhook-test-scaffold:latest
|
||||
environment:
|
||||
APP_PATH: /tmp/decline.json
|
||||
ports:
|
||||
- "3100:3000/tcp"
|
||||
volumes:
|
||||
@@ -87,54 +85,6 @@ services:
|
||||
fs:
|
||||
ipv4_address: 172.38.0.60
|
||||
|
||||
webhook-say:
|
||||
image: jambonz/webhook-test-scaffold:latest
|
||||
environment:
|
||||
APP_PATH: /tmp/say.json
|
||||
ports:
|
||||
- "3101:3000/tcp"
|
||||
volumes:
|
||||
- ./test-apps:/tmp
|
||||
networks:
|
||||
fs:
|
||||
ipv4_address: 172.38.0.61
|
||||
|
||||
webhook-gather:
|
||||
image: jambonz/webhook-test-scaffold:latest
|
||||
environment:
|
||||
APP_PATH: /tmp/gather.json
|
||||
ports:
|
||||
- "3102:3000/tcp"
|
||||
volumes:
|
||||
- ./test-apps:/tmp
|
||||
networks:
|
||||
fs:
|
||||
ipv4_address: 172.38.0.62
|
||||
|
||||
webhook-transcribe:
|
||||
image: jambonz/webhook-test-scaffold:latest
|
||||
environment:
|
||||
APP_PATH: /tmp/transcribe.json
|
||||
ports:
|
||||
- "3103:3000/tcp"
|
||||
volumes:
|
||||
- ./test-apps:/tmp
|
||||
networks:
|
||||
fs:
|
||||
ipv4_address: 172.38.0.63
|
||||
|
||||
webhook-sip-info:
|
||||
image: jambonz/webhook-test-scaffold:latest
|
||||
environment:
|
||||
APP_PATH: /tmp/info.json
|
||||
ports:
|
||||
- "3104:3000/tcp"
|
||||
volumes:
|
||||
- ./test-apps:/tmp
|
||||
networks:
|
||||
fs:
|
||||
ipv4_address: 172.38.0.64
|
||||
|
||||
influxdb:
|
||||
image: influxdb:1.8
|
||||
ports:
|
||||
|
||||
@@ -3,6 +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')
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
@@ -22,9 +23,25 @@ test('\'gather\' and \'transcribe\' tests', async(t) => {
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10');
|
||||
let obj = await getJSON('http://127.0.0.1:3102/actionHook');
|
||||
t.ok(obj.speech.alternatives[0].transcript = 'I\'d like to speak to customer support',
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "gather",
|
||||
"input": ["speech"],
|
||||
"recognizer": {
|
||||
"vendor": "google",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||
},
|
||||
"timeout": 10,
|
||||
"actionHook": "/actionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.speech.alternatives[0].transcript = 'I\'d like to speak to customer support',
|
||||
'gather: succeeds when using account credentials');
|
||||
|
||||
disconnect();
|
||||
|
||||
@@ -6,5 +6,7 @@ require('./webhooks-tests');
|
||||
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
198
test/play-tests.js
Normal 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);
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
const test = require('tape');
|
||||
const { sippUac } = require('./sipp')('test_fs');
|
||||
const clearModule = require('clear-module');
|
||||
const {provisionCallHook} = require('./utils')
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
@@ -20,9 +21,21 @@ test('\'say\' tests', async(t) => {
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
await sippUac('uac-say-account-creds-success.xml', '172.38.0.10');
|
||||
|
||||
// GIVEN
|
||||
const verbs = [
|
||||
{
|
||||
verb: 'say',
|
||||
text: 'hello'
|
||||
}
|
||||
];
|
||||
|
||||
const from = 'say_test_success';
|
||||
provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
t.pass('say: succeeds when using using account credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
INVITE sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: <sip:16174000000@[remote_ip]:[remote_port]>
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 INVITE
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
ACK sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
|
||||
[last_Via]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: <sip:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 ACK
|
||||
@@ -53,4 +53,3 @@
|
||||
</send>
|
||||
|
||||
</scenario>
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
INVITE sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
|
||||
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: <sip:16174000003@[remote_ip]:[remote_port]>
|
||||
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:sipp@[local_ip]:[local_port]
|
||||
Contact: sip:[from]@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
|
||||
Subject: uac-gather-account-creds-success
|
||||
@@ -53,13 +53,13 @@
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
ACK sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
|
||||
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: 16174000003 <sip:16174000003@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||
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:sipp@[local_ip]:[local_port]
|
||||
Contact: sip:[from]@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-gather-account-creds-success
|
||||
Content-Length: 0
|
||||
|
||||
71
test/scenarios/uac-invite-expect-480.xml
Normal file
71
test/scenarios/uac-invite-expect-480.xml
Normal 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>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
INVITE sip:16174000006@[remote_ip]:[remote_port] SIP/2.0
|
||||
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: <sip:16174000006@[remote_ip]:[remote_port]>
|
||||
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:sipp@[local_ip]:[local_port]
|
||||
Contact: sip:[from]@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
|
||||
Subject: uac-gather-account-creds-success
|
||||
@@ -53,13 +53,13 @@
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
ACK sip:16174000006@[remote_ip]:[remote_port] SIP/2.0
|
||||
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: 16174000006 <sip:16174000006@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||
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:sipp@[local_ip]:[local_port]
|
||||
Contact: sip:[from]@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-gather-account-creds-success
|
||||
Content-Length: 0
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
INVITE sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
|
||||
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: <sip:16174000001@[remote_ip]:[remote_port]>
|
||||
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:sipp@[local_ip]:[local_port]
|
||||
Contact: sip:[from]@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
|
||||
Subject: uac-say
|
||||
@@ -53,13 +53,13 @@
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
ACK sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
|
||||
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||
To: 16174000001 <sip:16174000001@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||
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:sipp@[local_ip]:[local_port]
|
||||
Contact: sip:[from]@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-say
|
||||
Content-Length: 0
|
||||
92
test/scenarios/uac-success-send-bye.xml
Normal file
92
test/scenarios/uac-success-send-bye.xml
Normal 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>
|
||||
|
||||
99
test/scenarios/uas-timeout-cancel.xml
Normal file
99
test/scenarios/uas-timeout-cancel.xml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
|
||||
<!-- This program is free software; you can redistribute it and/or -->
|
||||
<!-- modify it under the terms of the GNU General Public License as -->
|
||||
<!-- published by the Free Software Foundation; either version 2 of the -->
|
||||
<!-- License, or (at your option) any later version. -->
|
||||
<!-- -->
|
||||
<!-- This program is distributed in the hope that it will be useful, -->
|
||||
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
||||
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
||||
<!-- GNU General Public License for more details. -->
|
||||
<!-- -->
|
||||
<!-- You should have received a copy of the GNU General Public License -->
|
||||
<!-- along with this program; if not, write to the -->
|
||||
<!-- Free Software Foundation, Inc., -->
|
||||
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
|
||||
<!-- -->
|
||||
<!-- Sipp default 'uas' scenario. -->
|
||||
<!-- -->
|
||||
|
||||
<scenario name="UAS Timeout Receive Cancel">
|
||||
<!-- 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 request="INVITE" crlf="true">
|
||||
<action>
|
||||
<ereg regexp=".*" search_in="hdr" header="Subject:" assign_to="1" />
|
||||
<ereg regexp=".*" search_in="hdr" header="CSeq:" check_it="false" assign_to="2"/>
|
||||
</action>
|
||||
</recv>
|
||||
|
||||
<!-- The '[last_*]' keyword is replaced automatically by the -->
|
||||
<!-- specified header if it was present in the last message received -->
|
||||
<!-- (except if it was a retransmission). If the header was not -->
|
||||
<!-- present or if no message has been received, the '[last_*]' -->
|
||||
<!-- keyword is discarded, and all bytes until the end of the line -->
|
||||
<!-- are also discarded. -->
|
||||
<!-- -->
|
||||
<!-- If the specified header was present several times in the -->
|
||||
<!-- message, all occurrences are concatenated (CRLF separated) -->
|
||||
<!-- to be used in place of the '[last_*]' keyword. -->
|
||||
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
SIP/2.0 180 Ringing
|
||||
[last_Via:]
|
||||
[last_From:]
|
||||
[last_To:];tag=[pid]SIPpTag01[call_number]
|
||||
[last_Call-ID:]
|
||||
[last_CSeq:]
|
||||
[last_Record-Route:]
|
||||
Subject:[$1]
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv request="CANCEL" >
|
||||
</recv>
|
||||
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
SIP/2.0 200 OK
|
||||
[last_Via:]
|
||||
[last_From:]
|
||||
[last_To:];tag=[pid]SIPpTag01[call_number]
|
||||
[last_Call-ID:]
|
||||
[last_CSeq:]
|
||||
Subject:[$1]
|
||||
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
SIP/2.0 487 Request Terminated
|
||||
[last_Via:]
|
||||
[last_From:]
|
||||
[last_To:];tag=[pid]SIPpTag01[call_number]
|
||||
[last_Call-ID:]
|
||||
CSeq: [$2]
|
||||
[last_Record-Route:]
|
||||
Subject:[$1]
|
||||
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv request="ACK"
|
||||
rtd="true"
|
||||
crlf="true">
|
||||
</recv>
|
||||
</scenario>
|
||||
|
||||
@@ -3,6 +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')
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
@@ -22,9 +23,28 @@ test('sending SIP in-dialog requests tests', async(t) => {
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10');
|
||||
const obj = await getJSON('http://127.0.0.1:3104/actionHook');
|
||||
t.ok(obj.result === 'success' && obj.sip_status === 200, 'successfully sent SIP INFO');
|
||||
//GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "say",
|
||||
"text": "hello"
|
||||
},
|
||||
{
|
||||
"verb": "sip:request",
|
||||
"method": "info",
|
||||
"headers": {
|
||||
"Content-Type": "application/text"
|
||||
},
|
||||
"body": "here I am ",
|
||||
"actionHook": "/actionHook"
|
||||
}
|
||||
];
|
||||
let from = "sip_indialog_test";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10', from);
|
||||
const obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.sip_status === 200, 'successfully sent SIP INFO');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
|
||||
@@ -24,7 +24,7 @@ obj.output = () => {
|
||||
return output;
|
||||
};
|
||||
|
||||
obj.sippUac = (file, bindAddress) => {
|
||||
obj.sippUac = (file, bindAddress, from='sipp', to='16174000000') => {
|
||||
const cmd = 'docker';
|
||||
const args = [
|
||||
'run', '-t', '--rm', '--net', `${network}`,
|
||||
@@ -34,7 +34,9 @@ obj.sippUac = (file, bindAddress) => {
|
||||
'-sleep', '250ms',
|
||||
'-nostdin',
|
||||
'-cid_str', `%u-%p@%s-${idx++}`,
|
||||
'172.38.0.50'
|
||||
'172.38.0.50',
|
||||
'-key','from', from,
|
||||
'-key','to', to, '-trace_msg'
|
||||
];
|
||||
|
||||
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
|
||||
@@ -61,7 +63,7 @@ obj.sippUac = (file, bindAddress) => {
|
||||
addOutput(data.toString());
|
||||
});
|
||||
child_process.stdout.on('data', (data) => {
|
||||
//console.log(`stdout: ${data}`);
|
||||
// console.log(`stdout: ${data}`);
|
||||
addOutput(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
27
test/utils.js
Normal file
27
test/utils.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const bent = require('bent');
|
||||
|
||||
/*
|
||||
* phoneNumber: 16174000000
|
||||
* Hook endpoints http://127.0.0.1:3100/
|
||||
* The function help testcase to register desired jambonz json response for an application call
|
||||
* When a call has From number match the registered hook event, the desired jambonz json will be responded.
|
||||
*/
|
||||
const provisionCallHook = (from, verbs) => {
|
||||
const mapping = {
|
||||
from,
|
||||
data: JSON.stringify(verbs)
|
||||
};
|
||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||
post('/appMapping', mapping);
|
||||
}
|
||||
|
||||
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}
|
||||
@@ -1,15 +1,23 @@
|
||||
FROM node:alpine as builder
|
||||
RUN apk update && apk add --no-cache python make g++
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm prune
|
||||
FROM --platform=linux/amd64 node:18.6.0-alpine as base
|
||||
|
||||
FROM node:alpine as webapp
|
||||
RUN apk add curl
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
WORKDIR /opt/app/
|
||||
|
||||
FROM base as build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
FROM base
|
||||
|
||||
COPY --from=build /opt/app /opt/app/
|
||||
|
||||
ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
CMD [ "node", "app.js" ]
|
||||
|
||||
@@ -3,47 +3,117 @@ const fs = require('fs');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const listenPort = process.env.HTTP_PORT || 3000;
|
||||
let lastAction, lastEvent;
|
||||
|
||||
assert.ok(process.env.APP_PATH, 'env var APP_PATH is required');
|
||||
let json_mapping = new Map();
|
||||
let hook_mapping = new Map();
|
||||
|
||||
app.listen(listenPort, () => {
|
||||
console.log(`sample jambones app server listening on ${listenPort}`);
|
||||
});
|
||||
|
||||
const applicationData = JSON.parse(fs.readFileSync(process.env.APP_PATH));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
/*
|
||||
* Markup language
|
||||
*/
|
||||
|
||||
app.all('/', (req, res) => {
|
||||
console.log(applicationData, `${req.method} /`);
|
||||
return res.json(applicationData);
|
||||
console.log(req.body, 'POST /');
|
||||
const key = req.body.from
|
||||
return getJsonFromMap(key, req, res);
|
||||
});
|
||||
|
||||
app.post('/appMapping', (req, res) => {
|
||||
console.log(req.body, 'POST /appMapping');
|
||||
json_mapping.set(req.body.from, req.body.data);
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
/*
|
||||
* Status Callback
|
||||
*/
|
||||
app.post('/callStatus', (req, res) => {
|
||||
console.log({payload: req.body}, 'POST /callStatus');
|
||||
let key = req.body.from + "_callStatus"
|
||||
addRequestToMap(key, req, hook_mapping);
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
/*
|
||||
* action Hook
|
||||
*/
|
||||
app.post('/actionHook', (req, res) => {
|
||||
console.log({payload: req.body}, 'POST /actionHook');
|
||||
lastAction = req.body;
|
||||
let key = req.body.from + "_actionHook"
|
||||
addRequestToMap(key, req, hook_mapping);
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/actionHook', (req, res) => {
|
||||
console.log({payload: lastAction}, 'GET /actionHook');
|
||||
return res.json(lastAction);
|
||||
/*
|
||||
* 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('/eventHook', (req, res) => {
|
||||
console.log({payload: req.body}, 'POST /eventHook');
|
||||
lastEvent = req.body;
|
||||
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);
|
||||
});
|
||||
|
||||
app.get('/eventHook', (req, res) => {
|
||||
console.log({payload: lastEvent}, 'GET /eventHook');
|
||||
return res.json(lastEvent);
|
||||
});
|
||||
// Fetch Requests
|
||||
app.get('/requests/:key', (req, res) => {
|
||||
let key = req.params.key;
|
||||
if (hook_mapping.has(key)) {
|
||||
return res.json(hook_mapping.get(key));
|
||||
} else {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
app.get('/lastRequest/:key', (req, res) => {
|
||||
let key = req.params.key;
|
||||
if (hook_mapping.has(key)) {
|
||||
let requests = hook_mapping.get(key);
|
||||
return res.json(requests[requests.length - 1]);
|
||||
} else {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* 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++) {
|
||||
if (i % 2 === 0) {
|
||||
headers.set(req.rawHeaders[i], req.rawHeaders[i + 1])
|
||||
}
|
||||
}
|
||||
let request = {
|
||||
'url': req.url,
|
||||
'headers': Object.fromEntries(headers),
|
||||
'body': req.body
|
||||
}
|
||||
if (map.has(key)) {
|
||||
map.get(key).push(request);
|
||||
} else {
|
||||
map.set(key, [request]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /opt/app/
|
||||
npm start
|
||||
487
test/webhook/package-lock.json
generated
487
test/webhook/package-lock.json
generated
@@ -1,8 +1,493 @@
|
||||
{
|
||||
"name": "webhook",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "webhook",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.24",
|
||||
"negotiator": "0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.0",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "~2.3.0",
|
||||
"qs": "6.7.0",
|
||||
"raw-body": "2.4.0",
|
||||
"type-is": "~1.6.17"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
|
||||
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
|
||||
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.7",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.19.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.1.2",
|
||||
"fresh": "0.5.2",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.5",
|
||||
"qs": "6.7.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.1.2",
|
||||
"send": "0.17.1",
|
||||
"serve-static": "1.14.1",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": "~1.5.0",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.3.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~1.5.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
|
||||
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.3",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.28",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
|
||||
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.45.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
|
||||
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
|
||||
"dependencies": {
|
||||
"forwarded": "~0.1.2",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
|
||||
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.0",
|
||||
"http-errors": "1.7.2",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "~1.1.2",
|
||||
"destroy": "~1.0.4",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "~1.7.2",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.1",
|
||||
"on-finished": "~2.3.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
|
||||
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
|
||||
@@ -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);
|
||||
@@ -20,7 +29,21 @@ test('basic webhook tests', async(t) => {
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
await sippUac('uac-expect-603.xml', '172.38.0.10');
|
||||
const verbs = [
|
||||
{
|
||||
verb: 'sip:decline',
|
||||
status: 603,
|
||||
reason: 'Gone Fishin',
|
||||
headers: {
|
||||
'Retry-After': 300
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const from = 'sip_decline_test_success';
|
||||
provisionCallHook(from, verbs)
|
||||
|
||||
await sippUac('uac-expect-603.xml', '172.38.0.10', from);
|
||||
t.pass('webhook successfully declines call');
|
||||
|
||||
disconnect();
|
||||
@@ -30,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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user