mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-15 02:39:35 +00:00
Compare commits
48 Commits
v0.7.2-rc2
...
feature/ga
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b31a8db9de | ||
|
|
1d1388583b | ||
|
|
942c73e64b | ||
|
|
de61cc1d3d | ||
|
|
32ebc7017d | ||
|
|
87ef510dea | ||
|
|
22440843d9 | ||
|
|
48fa3133ef | ||
|
|
a3352cc932 | ||
|
|
2de54ddedf | ||
|
|
812b40fcb0 | ||
|
|
9a0e33e4f7 | ||
|
|
c7e141abf1 | ||
|
|
e30701c4b4 | ||
|
|
c5d392af6a | ||
|
|
3c5d392407 | ||
|
|
5bfc451c85 | ||
|
|
47478fd409 | ||
|
|
c16a2662f2 | ||
|
|
c1130adf03 | ||
|
|
f982f6c7d8 | ||
|
|
f20190b0fc | ||
|
|
74e85e1b16 | ||
|
|
63e9cb985e | ||
|
|
2e88ab1f55 | ||
|
|
7f75a35515 | ||
|
|
941727e93f | ||
|
|
d8bfa33a00 | ||
|
|
30ed5b6a02 | ||
|
|
bac1b7f2c6 | ||
|
|
48deb3ae89 | ||
|
|
de83f735ea | ||
|
|
cfe9397502 | ||
|
|
dda3335060 | ||
|
|
2329f0cda0 | ||
|
|
36683dc151 | ||
|
|
ce738a7852 | ||
|
|
77a696a0dc | ||
|
|
62ff44540d | ||
|
|
e5821cddf8 | ||
|
|
25567a7842 | ||
|
|
40bd3c9c88 | ||
|
|
27d6d32359 | ||
|
|
142f5d409f | ||
|
|
da4a7184a4 | ||
|
|
2c72bf50cd | ||
|
|
b27f349fc6 | ||
|
|
138aa5836a |
@@ -1,4 +1,4 @@
|
||||
FROM node:17-slim
|
||||
FROM node:17.4-slim
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
3
app.js
3
app.js
@@ -31,6 +31,7 @@ const {
|
||||
|
||||
// HTTP
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const app = express();
|
||||
Object.assign(app.locals, {
|
||||
logger,
|
||||
@@ -73,6 +74,8 @@ srf.invite((req, res) => {
|
||||
});
|
||||
|
||||
// HTTP
|
||||
app.use(helmet());
|
||||
app.use(helmet.hidePoweredBy());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use('/', httpRoutes);
|
||||
|
||||
@@ -6,7 +6,8 @@ const {CallDirection, CallStatus} = require('../../utils/constants');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const SipError = require('drachtio-srf').SipError;
|
||||
const sysError = require('./error');
|
||||
const Requestor = require('../../utils/requestor');
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
const WsRequestor = require('../../utils/ws-requestor');
|
||||
const dbUtils = require('../../utils/db-utils');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
@@ -35,6 +36,8 @@ router.post('/', async(req, res) => {
|
||||
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
'X-Jambonz-Routing': target.type,
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
'X-Call-Sid': callSid,
|
||||
'X-Account-Sid': req.body.account_sid
|
||||
};
|
||||
@@ -102,11 +105,16 @@ router.post('/', async(req, res) => {
|
||||
* attach our requestor and notifier objects
|
||||
* these will be used for all http requests we make during this call
|
||||
*/
|
||||
app.requestor = new Requestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||
if (app.call_status_hook) {
|
||||
app.notifier = new Requestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
||||
if ('WS' === app.call_hook?.method) {
|
||||
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook,
|
||||
account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
}
|
||||
else app.notifier = {request: () => {}};
|
||||
|
||||
/* now launch the outdial */
|
||||
try {
|
||||
@@ -120,6 +128,7 @@ router.post('/', async(req, res) => {
|
||||
res.status(500).send('Call Failure');
|
||||
return;
|
||||
}
|
||||
inviteReq.srf = srf;
|
||||
/* ok our outbound INVITE is in flight */
|
||||
|
||||
const tasks = [restDial];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const Requestor = require('../../utils/requestor');
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
const WsRequestor = require('../../utils/ws-requestor');
|
||||
const CallInfo = require('../../session/call-info');
|
||||
const {CallDirection} = require('../../utils/constants');
|
||||
const SmsSession = require('../../session/sms-call-session');
|
||||
@@ -18,7 +19,17 @@ router.post('/:partner', async(req, res) => {
|
||||
const app = req.body.app;
|
||||
const account = await lookupAccountBySid(app.accountSid);
|
||||
const hook = app.messaging_hook;
|
||||
const requestor = new Requestor(logger, account.account_sid, hook, account.webhook_secret);
|
||||
let requestor;
|
||||
|
||||
if ('WS' === hook?.method) {
|
||||
app.requestor = new WsRequestor(logger, account.account_sid, hook, account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account.account_sid, hook, account.webhook_secret);
|
||||
app.notifier = {request: () => {}};
|
||||
}
|
||||
|
||||
const payload = {
|
||||
carrier: req.params.partner,
|
||||
messageSid: app.messageSid,
|
||||
@@ -33,7 +44,7 @@ router.post('/:partner', async(req, res) => {
|
||||
res.status(200).json({sid: req.body.messageSid});
|
||||
|
||||
try {
|
||||
tasks = await requestor.request(hook, payload);
|
||||
tasks = await requestor.request('session:new', hook, payload);
|
||||
logger.info({tasks}, 'response from incoming SMS webhook');
|
||||
} catch (err) {
|
||||
logger.error({err, hook}, 'Error sending incoming SMS message');
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const {CallDirection} = require('./utils/constants');
|
||||
const CallInfo = require('./session/call-info');
|
||||
const Requestor = require('./utils/requestor');
|
||||
const HttpRequestor = require('./utils/http-requestor');
|
||||
const WsRequestor = require('./utils/ws-requestor');
|
||||
const makeTask = require('./tasks/make_task');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const normalizeJambones = require('./utils/normalize-jambones');
|
||||
@@ -105,7 +106,7 @@ module.exports = function(srf, logger) {
|
||||
}
|
||||
else {
|
||||
const uri = parseUri(req.uri);
|
||||
const arr = /context-(.*)/.exec(uri.user);
|
||||
const arr = /context-(.*)/.exec(uri?.user);
|
||||
if (arr) {
|
||||
// this is a transfer from another feature server
|
||||
const {retrieveKey, deleteKey} = srf.locals.dbHelpers;
|
||||
@@ -142,10 +143,18 @@ module.exports = function(srf, logger) {
|
||||
* create a requestor that we will use for all http requests we make during the call.
|
||||
* also create a notifier for call status events (if not needed, its a no-op).
|
||||
*/
|
||||
app.requestor = new Requestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new Requestor(logger, account_sid, app.call_status_hook,
|
||||
accountInfo.account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
if ('WS' === app.call_hook?.method ||
|
||||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
|
||||
app.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
app.call_hook.method = 'WS';
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
|
||||
accountInfo.account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
}
|
||||
|
||||
req.locals.application = app;
|
||||
const obj = Object.assign({}, app);
|
||||
@@ -176,15 +185,15 @@ module.exports = function(srf, logger) {
|
||||
return next();
|
||||
}
|
||||
/* retrieve the application to execute for this inbound call */
|
||||
const params = Object.assign(app.call_hook.method === 'POST' ? {sip: req.msg} : {},
|
||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
|
||||
req.locals.callInfo);
|
||||
const json = await app.requestor.request(app.call_hook, params);
|
||||
const json = await app.requestor.request('session:new', app.call_hook, params);
|
||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err.message}});
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err?.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const {CallDirection, CallStatus} = require('../utils/constants');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
/**
|
||||
* @classdesc Represents the common information for all calls
|
||||
* that is provided in call status webhooks
|
||||
@@ -9,6 +8,7 @@ const { v4: uuidv4 } = require('uuid');
|
||||
class CallInfo {
|
||||
constructor(opts) {
|
||||
let from ;
|
||||
let srf;
|
||||
this.direction = opts.direction;
|
||||
if (opts.req) {
|
||||
const u = opts.req.getParsedHeader('from');
|
||||
@@ -19,6 +19,7 @@ class CallInfo {
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
// inbound call
|
||||
const {app, req} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = req.locals.callSid,
|
||||
this.accountSid = app.account_sid,
|
||||
this.applicationSid = app.application_sid;
|
||||
@@ -33,6 +34,7 @@ class CallInfo {
|
||||
else if (opts.parentCallInfo) {
|
||||
// outbound call that is a child of an existing call
|
||||
const {req, parentCallInfo, to, callSid} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = callSid || uuidv4();
|
||||
this.parentCallSid = parentCallInfo.callSid;
|
||||
this.accountSid = parentCallInfo.accountSid;
|
||||
@@ -47,6 +49,7 @@ class CallInfo {
|
||||
else if (this.direction === CallDirection.None) {
|
||||
// outbound SMS
|
||||
const {messageSid, accountSid, applicationSid, res} = opts;
|
||||
srf = res.srf;
|
||||
this.messageSid = messageSid;
|
||||
this.accountSid = accountSid;
|
||||
this.applicationSid = applicationSid;
|
||||
@@ -55,6 +58,7 @@ class CallInfo {
|
||||
else {
|
||||
// outbound call triggered by REST
|
||||
const {req, callSid, accountSid, applicationSid, to, tag} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = callSid;
|
||||
this.accountSid = accountSid;
|
||||
this.applicationSid = applicationSid;
|
||||
@@ -65,6 +69,11 @@ class CallInfo {
|
||||
this.to = to;
|
||||
if (tag) this._customerData = tag;
|
||||
}
|
||||
|
||||
this.localSipAddress = srf.locals.localSipAddress;
|
||||
if (srf.locals.publicIp) {
|
||||
this.publicIp = srf.locals.publicIp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +109,8 @@ class CallInfo {
|
||||
callStatus: this.callStatus,
|
||||
callerId: this.callerId,
|
||||
accountSid: this.accountSid,
|
||||
applicationSid: this.applicationSid
|
||||
applicationSid: this.applicationSid,
|
||||
fsSipAddress: this.localSipAddress
|
||||
};
|
||||
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
|
||||
if (this[prop]) obj[prop] = this[prop];
|
||||
@@ -110,6 +120,13 @@ class CallInfo {
|
||||
if (this._customerData) {
|
||||
Object.assign(obj, {customerData: this._customerData});
|
||||
}
|
||||
|
||||
if (process.env.JAMBONES_API_BASE_URL) {
|
||||
Object.assign(obj, {apiBaseUrl: process.env.JAMBONES_API_BASE_URL});
|
||||
}
|
||||
if (this.publicIp) {
|
||||
Object.assign(obj, {fsPublicIp: this.publicIp});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ const sessionTracker = require('./session-tracker');
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const listTaskNames = require('../utils/summarize-tasks');
|
||||
const Requestor = require('../utils/requestor');
|
||||
const HttpRequestor = require('../utils/http-requestor');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
||||
|
||||
@@ -48,6 +49,7 @@ class CallSession extends Emitter {
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx = 0;
|
||||
this.callGone = false;
|
||||
this.notifiedComplete = false;
|
||||
|
||||
this.tmpFiles = new Set();
|
||||
|
||||
@@ -61,6 +63,8 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
this._pool = srf.locals.dbHelpers.pool;
|
||||
|
||||
this.requestor.on('command', this._onCommand.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,6 +268,12 @@ class CallSession extends Emitter {
|
||||
region: credential.region
|
||||
};
|
||||
}
|
||||
else if ('wellsaid' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
writeAlerts({
|
||||
@@ -282,6 +292,7 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
async exec() {
|
||||
this.logger.info({tasks: listTaskNames(this.tasks)}, `CallSession:exec starting ${this.tasks.length} tasks`);
|
||||
|
||||
while (this.tasks.length && !this.callGone) {
|
||||
const taskNum = ++this.taskIdx;
|
||||
const stackNum = this.stackIdx;
|
||||
@@ -295,7 +306,7 @@ class CallSession extends Emitter {
|
||||
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
} catch (err) {
|
||||
this.currentTask = null;
|
||||
if (err.message.includes(BADPRECONDITIONS)) {
|
||||
if (err.message?.includes(BADPRECONDITIONS)) {
|
||||
this.logger.info(`CallSession:exec task #${stackNum}:${taskNum}: ${task.name}: ${err.message}`);
|
||||
}
|
||||
else {
|
||||
@@ -303,6 +314,16 @@ class CallSession extends Emitter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === this.tasks.length && this.hasStableDialog && this.requestor instanceof WsRequestor) {
|
||||
try {
|
||||
await this._awaitCommandsOrHangup();
|
||||
if (!this.hasStableDialog || this.callGone) break;
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// all done - cleanup
|
||||
@@ -361,6 +382,10 @@ class CallSession extends Emitter {
|
||||
this.currentTask.kill(this);
|
||||
this.currentTask = null;
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,29 +422,42 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
async _lccCallHook(opts) {
|
||||
const webhooks = [];
|
||||
let sd;
|
||||
if (opts.call_hook) webhooks.push(this.requestor.request(opts.call_hook, this.callInfo.toJSON()));
|
||||
if (opts.child_call_hook) {
|
||||
/* child call hook only allowed from a connected Dial state */
|
||||
const task = this.currentTask;
|
||||
sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
webhooks.push(this.requestor.request(opts.child_call_hook, sd.callInfo.toJSON()));
|
||||
let sd, tasks, childTasks;
|
||||
|
||||
if (opts.call_hook || opts.child_call_hook) {
|
||||
if (opts.call_hook) {
|
||||
webhooks.push(this.requestor.request('session:redirect', opts.call_hook, this.callInfo.toJSON()));
|
||||
}
|
||||
if (opts.child_call_hook) {
|
||||
/* child call hook only allowed from a connected Dial state */
|
||||
const task = this.currentTask;
|
||||
sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
webhooks.push(this.requestor.request('session:redirect', opts.child_call_hook, sd.callInfo.toJSON()));
|
||||
}
|
||||
}
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
if (opts.call_hook) {
|
||||
tasks = tasks1;
|
||||
if (opts.child_call_hook) childTasks = tasks2;
|
||||
}
|
||||
else childTasks = tasks1;
|
||||
}
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
let tasks, childTasks;
|
||||
if (opts.call_hook) {
|
||||
tasks = tasks1;
|
||||
if (opts.child_call_hook) childTasks = tasks2;
|
||||
else if (opts.parent_call || opts.child_call) {
|
||||
const {parent_call, child_call} = opts;
|
||||
assert.ok(!parent_call || Array.isArray(parent_call), 'CallSession:_lccCallHook - parent_call must be an array');
|
||||
assert.ok(!child_call || Array.isArray(child_call), 'CallSession:_lccCallHook - child_call must be an array');
|
||||
tasks = parent_call;
|
||||
childTasks = child_call;
|
||||
}
|
||||
else childTasks = tasks1;
|
||||
|
||||
if (childTasks) {
|
||||
const {parentLogger} = this.srf.locals;
|
||||
const childLogger = parentLogger.child({callId: this.callId, callSid: sd.callSid});
|
||||
const t = normalizeJambones(childLogger, childTasks).map((tdata) => makeTask(childLogger, tdata));
|
||||
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list for child call');
|
||||
|
||||
// TODO: if using websockets api, we need a new websocket for the adulting session..
|
||||
const cs = await sd.doAdulting({
|
||||
logger: childLogger,
|
||||
application: this.application,
|
||||
@@ -597,6 +635,59 @@ class CallSession extends Emitter {
|
||||
this.taskIdx = 0;
|
||||
}
|
||||
|
||||
_onCommand({msgid, command, queueCommand, data}) {
|
||||
this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command');
|
||||
switch (command) {
|
||||
case 'redirect':
|
||||
if (Array.isArray(data)) {
|
||||
const t = normalizeJambones(this.logger, data).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (!queueCommand) {
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list');
|
||||
this.replaceApplication(t);
|
||||
}
|
||||
else {
|
||||
this.logger.info({t, tasks: this.tasks}, 'CallSession:_onCommand - about to queue tasks');
|
||||
this.tasks.push(...t);
|
||||
this.logger.debug({tasks: this.tasks}, 'CallSession:_onCommand - tasks have been queued');
|
||||
}
|
||||
}
|
||||
else this._lccCallHook(data);
|
||||
break;
|
||||
|
||||
case 'call:status':
|
||||
this._lccCallStatus(data);
|
||||
break;
|
||||
|
||||
case 'mute:status':
|
||||
this._lccMuteStatus(data);
|
||||
break;
|
||||
|
||||
case 'conf:mute-status':
|
||||
this._lccConfMuteStatus(data);
|
||||
break;
|
||||
|
||||
case 'conf:hold-status':
|
||||
this._lccConfHoldStatus(data);
|
||||
break;
|
||||
|
||||
case 'listen:status':
|
||||
this._lccListenStatus(data);
|
||||
break;
|
||||
|
||||
case 'whisper':
|
||||
this._lccWhisper(data);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.logger.info('CallSession:_onCommand - got commands, waking up..');
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
|
||||
_evaluatePreconditions(task) {
|
||||
switch (task.preconditions) {
|
||||
case TaskPreconditions.None:
|
||||
@@ -733,6 +824,7 @@ class CallSession extends Emitter {
|
||||
});
|
||||
}
|
||||
this.tmpFiles.clear();
|
||||
this.requestor && this.requestor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -834,7 +926,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
else {
|
||||
this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found');
|
||||
this.queueEventHookRequestor = new Requestor(this.logger, this.accountSid,
|
||||
this.queueEventHookRequestor = new HttpRequestor(this.logger, this.accountSid,
|
||||
r[0], this.webhook_secret);
|
||||
this.queueEventHook = r[0];
|
||||
}
|
||||
@@ -848,7 +940,7 @@ class CallSession extends Emitter {
|
||||
/* send webhook */
|
||||
const params = {...obj, ...this.callInfo.toJSON()};
|
||||
this.logger.info({accountSid: this.accountSid, params}, 'performQueueWebhook: sending webhook');
|
||||
this.queueEventHookRequestor.request(this.queueEventHook, params)
|
||||
this.queueEventHookRequestor.request('queue:status', this.queueEventHook, params)
|
||||
.catch((err) => {
|
||||
this.logger.info({err, accountSid: this.accountSid, obj}, 'Error sending queue notification event');
|
||||
});
|
||||
@@ -937,6 +1029,10 @@ class CallSession extends Emitter {
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('CallSession: call terminated by jambones');
|
||||
origDestroy();
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -982,6 +1078,12 @@ class CallSession extends Emitter {
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
if (this.callMoved) return;
|
||||
|
||||
/* race condition: we hang up at the same time as the caller */
|
||||
if (callStatus === CallStatus.Completed) {
|
||||
if (this.notifiedComplete) return;
|
||||
this.notifiedComplete = true;
|
||||
}
|
||||
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
@@ -989,7 +1091,7 @@ class CallSession extends Emitter {
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.notifier.request(this.call_status_hook, this.callInfo.toJSON());
|
||||
this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
@@ -999,6 +1101,14 @@ class CallSession extends Emitter {
|
||||
this.updateCallStatus(Object.assign({}, this.callInfo.toJSON()), this.serviceUrl)
|
||||
.catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
|
||||
_awaitCommandsOrHangup() {
|
||||
assert(!this.wakeupResolver);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.logger.info('_awaitCommandsOrHangup - waiting...');
|
||||
this.wakeupResolver = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallSession;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,7 +529,7 @@ class Conference extends Task {
|
||||
|
||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
|
||||
assert(!this._playSession);
|
||||
const json = await cs.application.requestor.request(hook, cs.callInfo);
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, cs.callInfo);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
@@ -586,7 +586,7 @@ class Conference extends Task {
|
||||
params.duration = (Date.now() - this.conferenceStartTime.getTime()) / 1000;
|
||||
if (!params.time) params.time = (new Date()).toISOString();
|
||||
if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount;
|
||||
cs.application.requestor.request(this.statusHook, Object.assign(params, this.statusParams))
|
||||
cs.application.requestor.request('verb:hook', this.statusHook, Object.assign(params, this.statusParams))
|
||||
.catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ class TaskDial extends Task {
|
||||
this.epOther.play(this.dialMusic).catch((err) => {});
|
||||
}
|
||||
}
|
||||
await this._attemptCalls(cs);
|
||||
if (!this.killed) await this._attemptCalls(cs);
|
||||
await this.awaitTaskDone();
|
||||
this.logger.debug({callSid: this.cs.callSid}, 'Dial:exec task is done, sending actionHook if any');
|
||||
await this.performAction(this.results, this.killReason !== KillReason.Replaced);
|
||||
@@ -179,6 +179,7 @@ class TaskDial extends Task {
|
||||
this._killOutdials();
|
||||
if (this.sd) {
|
||||
this.sd.kill();
|
||||
this.sd.removeAllListeners();
|
||||
this.sd = null;
|
||||
}
|
||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||
@@ -287,7 +288,7 @@ class TaskDial extends Task {
|
||||
const match = dtmfDetector.keyPress(key);
|
||||
if (match) {
|
||||
this.logger.info({callSid}, `Dial:_onInfo triggered dtmf match: ${match}`);
|
||||
requestor.request(this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
|
||||
requestor.request('verb:hook', this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
@@ -366,6 +367,9 @@ class TaskDial extends Task {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.killed) return;
|
||||
|
||||
const sd = placeCall({
|
||||
logger: this.logger,
|
||||
application: cs.application,
|
||||
@@ -381,7 +385,9 @@ class TaskDial extends Task {
|
||||
|
||||
sd
|
||||
.on('callCreateFail', () => {
|
||||
clearTimeout(this.timerRing);
|
||||
this.dials.delete(sd.callSid);
|
||||
sd.removeAllListeners();
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after call create err, ending task');
|
||||
this.kill(cs);
|
||||
@@ -423,6 +429,7 @@ class TaskDial extends Task {
|
||||
})
|
||||
.on('accept', async() => {
|
||||
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
||||
clearTimeout(this.timerRing);
|
||||
try {
|
||||
await this._connectSingleDial(cs, sd);
|
||||
} catch (err) {
|
||||
@@ -431,7 +438,9 @@ class TaskDial extends Task {
|
||||
})
|
||||
.on('decline', () => {
|
||||
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
|
||||
clearTimeout(this.timerRing);
|
||||
this.dials.delete(sd.callSid);
|
||||
sd.removeAllListeners();
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending task');
|
||||
this.kill(cs);
|
||||
@@ -554,8 +563,7 @@ class TaskDial extends Task {
|
||||
try {
|
||||
const aLegSdp = cs.ep.remote.sdp;
|
||||
const bLegSdp = sd.dlg.remote.sdp;
|
||||
this.logger.debug({aLegSdp, bLegSdp}, 'Dial:_releaseMedia - releasing media from freewitch');
|
||||
await Promise.all[sd.releaseMediaToSBC(aLegSdp), cs.releaseMediaToSBC(bLegSdp)];
|
||||
await Promise.all[sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp), cs.releaseMediaToSBC(bLegSdp)];
|
||||
this.epOther = null;
|
||||
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
|
||||
} catch (err) {
|
||||
|
||||
@@ -453,7 +453,7 @@ class Dialogflow extends Task {
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results = {}) {
|
||||
const json = await this.cs.requestor.request(hook, {...results, ...cs.callInfo.toJSON()});
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, {...results, ...cs.callInfo.toJSON()});
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('../make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -317,7 +317,7 @@ class TaskEnqueue extends Task {
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
|
||||
}
|
||||
const json = await cs.application.requestor.request(hook, params);
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, params);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
|
||||
const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7);
|
||||
|
||||
class TaskGather extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
@@ -16,12 +17,15 @@ class TaskGather extends Task {
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'finishOnKey', 'hints', 'input', 'numDigits',
|
||||
'partialResultHook',
|
||||
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
'speechTimeout', 'timeout', 'say', 'play'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
|
||||
this.timeout = (this.timeout || 5) * 1000;
|
||||
/* when collecting dtmf, bargein on dtmf is true unless explicitly set to false */
|
||||
if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true;
|
||||
|
||||
this.timeout = (this.timeout || 15) * 1000;
|
||||
this.interim = this.partialResultCallback;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
@@ -30,6 +34,10 @@ class TaskGather extends Task {
|
||||
this.hints = recognizer.hints || [];
|
||||
this.altLanguages = recognizer.altLanguages || [];
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
|
||||
/* aws options */
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
@@ -40,6 +48,10 @@ class TaskGather extends Task {
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
|
||||
/* barge in configuration */
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
|
||||
}
|
||||
|
||||
this.digitBuffer = '';
|
||||
@@ -48,6 +60,12 @@ class TaskGather extends Task {
|
||||
if (this.say) this.sayTask = makeTask(this.logger, {say: this.say}, this);
|
||||
if (this.play) this.playTask = makeTask(this.logger, {play: this.play}, this);
|
||||
|
||||
if (this.sayTask || this.playTask) {
|
||||
// this is specially for barge in where we want to make a bargebale promt
|
||||
// to a user without listening after the say task has finished
|
||||
this.listenAfterSpeech = typeof this.data.listenAfterSpeech === 'boolean' ? this.data.listenAfterSpeech : true;
|
||||
}
|
||||
|
||||
this.parentTask = parentTask;
|
||||
}
|
||||
|
||||
@@ -80,29 +98,56 @@ class TaskGather extends Task {
|
||||
throw new Error(`no speech-to-text service credentials for ${this.vendor} have been configured`);
|
||||
}
|
||||
|
||||
const startListening = (cs, ep) => {
|
||||
this._startTimer();
|
||||
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
||||
this._initSpeech(cs, ep)
|
||||
.then(() => {
|
||||
this._startTranscribing(ep);
|
||||
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.sayTask) {
|
||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.on('playDone', (err) => {
|
||||
if (!this.killed) this._startTimer();
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.info('Gather: say task completed');
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this.playTask) {
|
||||
this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.on('playDone', (err) => {
|
||||
if (!this.killed) this._startTimer();
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else this._startTimer();
|
||||
else startListening(cs, ep);
|
||||
|
||||
if (this.input.includes('speech')) {
|
||||
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
||||
await this._initSpeech(cs, ep);
|
||||
this._startTranscribing(ep);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
}
|
||||
|
||||
if (this.input.includes('digits')) {
|
||||
if (this.input.includes('digits') || this.dtmfBargein) {
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
|
||||
@@ -121,83 +166,111 @@ class TaskGather extends Task {
|
||||
super.kill(cs);
|
||||
this._killAudio(cs);
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
this._resolve('killed');
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
let resolved = false;
|
||||
if (this.dtmfBargein) this._killAudio(cs);
|
||||
if (evt.dtmf === this.finishOnKey && this.input.includes('digits')) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-terminator-key');
|
||||
}
|
||||
else {
|
||||
this.digitBuffer += evt.dtmf;
|
||||
if (this.digitBuffer.length === this.numDigits) this._resolve('dtmf-num-digits');
|
||||
const len = this.digitBuffer.length;
|
||||
if (len === this.numDigits || len === this.maxDigits) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-num-digits');
|
||||
}
|
||||
}
|
||||
if (!resolved && this.interDigitTimeout > 0 && this.digitBuffer.length >= this.minDigits) {
|
||||
/* start interDigitTimer */
|
||||
const ms = this.interDigitTimeout * 1000;
|
||||
this.logger.debug(`starting interdigit timer of ${ms}`);
|
||||
this.interDigitTimer = setTimeout(() => this._resolve('dtmf-interdigit-timeout'), ms);
|
||||
}
|
||||
this._killAudio(cs);
|
||||
}
|
||||
|
||||
async _initSpeech(cs, ep) {
|
||||
const opts = {};
|
||||
try {
|
||||
const opts = {};
|
||||
|
||||
if ('google' === this.vendor) {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
Object.assign(opts, {
|
||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
||||
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
|
||||
GOOGLE_SPEECH_MODEL: 'command_and_search'
|
||||
});
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
if (this.vad.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
if (this.altLanguages && this.altLanguages.length > 1) {
|
||||
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
if (this.profanityFilter === true) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
|
||||
}
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
|
||||
}
|
||||
else if (['aws', 'polly'].includes(this.vendor)) {
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
if (this.vocabularyFilterName) {
|
||||
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
|
||||
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
|
||||
}
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
}
|
||||
else if ('microsoft' === this.vendor) {
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
|
||||
'AZURE_REGION': this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
//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;
|
||||
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoSpeechDetected.bind(this, cs, ep));
|
||||
if ('google' === this.vendor) {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
Object.assign(opts, {
|
||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
||||
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
|
||||
GOOGLE_SPEECH_MODEL: 'command_and_search'
|
||||
});
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.altLanguages && this.altLanguages.length > 1) {
|
||||
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
if (this.profanityFilter === true) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
|
||||
}
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
|
||||
}
|
||||
else if (['aws', 'polly'].includes(this.vendor)) {
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
if (this.vocabularyFilterName) {
|
||||
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
|
||||
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
|
||||
}
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
}
|
||||
else if ('microsoft' === this.vendor) {
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
|
||||
'AZURE_REGION': this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
//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;
|
||||
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected,
|
||||
this._onNoSpeechDetected.bind(this, cs, ep));
|
||||
}
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.error(err, 'Error setting channel variables'));
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'could not init speech for listening');
|
||||
}
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
}
|
||||
|
||||
|
||||
_startTranscribing(ep) {
|
||||
ep.startTranscription({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
interim: this.partialResultCallback ? true : false,
|
||||
interim: this.partialResultCallback || this.bargein,
|
||||
}).catch((err) => {
|
||||
const {writeAlerts, AlertType} = this.cs.srf.locals;
|
||||
this.logger.error(err, 'TaskGather:_startTranscribing error');
|
||||
@@ -239,25 +312,50 @@ class TaskGather extends Task {
|
||||
_onTranscription(cs, ep, evt) {
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives: [
|
||||
{
|
||||
confidence: nbest[0].Confidence,
|
||||
transcript: nbest[0].Display
|
||||
}
|
||||
]
|
||||
};
|
||||
evt = newEvent;
|
||||
const final = evt.RecognitionStatus === 'Success';
|
||||
if (final) {
|
||||
const nbest = evt.NBest;
|
||||
evt = {
|
||||
is_final: true,
|
||||
alternatives: [
|
||||
{
|
||||
confidence: nbest[0].Confidence,
|
||||
transcript: nbest[0].Display
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
else {
|
||||
evt = {
|
||||
is_final: false,
|
||||
alternatives: [
|
||||
{
|
||||
transcript: evt.Text
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if (evt.is_final) this._resolve('speech', evt);
|
||||
else if (this.partialResultHook) {
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
|
||||
else {
|
||||
/* google has a measure of stability:
|
||||
https://cloud.google.com/speech-to-text/docs/basics#streaming_responses
|
||||
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) {
|
||||
this.logger.debug('Gather:_onTranscription - killing audio due to speech bargein');
|
||||
this._killAudio(cs);
|
||||
}
|
||||
if (this.partialResultHook) {
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onEndOfUtterance(cs, ep) {
|
||||
this.logger.info('TaskGather:_onEndOfUtterance');
|
||||
if (!this.resolved && !this.killed) {
|
||||
@@ -273,6 +371,7 @@ class TaskGather extends Task {
|
||||
if (this.resolved) return;
|
||||
this.resolved = true;
|
||||
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
|
||||
clearTimeout(this.interDigitTimer);
|
||||
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.ep.stopTranscription({vendor: this.vendor})
|
||||
|
||||
@@ -289,7 +289,7 @@ class Lex extends Task {
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results) {
|
||||
const json = await this.cs.requestor.request(hook, results);
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -42,7 +42,7 @@ class TaskMessage extends Task {
|
||||
}
|
||||
if (gw) {
|
||||
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
|
||||
url = getSmpp();
|
||||
url = process.env.K8S ? 'http://smpp' : getSmpp();
|
||||
relativeUrl = '/sms';
|
||||
payload = {
|
||||
...payload,
|
||||
|
||||
@@ -48,7 +48,7 @@ class TaskRestDial extends Task {
|
||||
cs.setDialog(dlg);
|
||||
|
||||
try {
|
||||
const tasks = await cs.requestor.request(this.call_hook, cs.callInfo);
|
||||
const tasks = await cs.requestor.request('verb:hook', this.call_hook, cs.callInfo);
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
||||
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const { logger } = require('../../app');
|
||||
|
||||
class TaskSay extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
@@ -22,15 +21,20 @@ class TaskSay extends Task {
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
const hasVerbLevelTts = this.synthesizer.vendor && this.synthesizer.vendor !== 'default';
|
||||
const vendor = hasVerbLevelTts ? this.synthesizer.vendor : cs.speechSynthesisVendor ;
|
||||
const language = hasVerbLevelTts ? this.synthesizer.language : cs.speechSynthesisLanguage ;
|
||||
const voice = hasVerbLevelTts ? this.synthesizer.voice : cs.speechSynthesisVoice ;
|
||||
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
|
||||
this.synthesizer.vendor :
|
||||
cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
|
||||
this.synthesizer.language :
|
||||
cs.speechSynthesisLanguage ;
|
||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||
this.synthesizer.voice :
|
||||
cs.speechSynthesisVoice;
|
||||
const engine = this.synthesizer.engine || 'standard';
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
this.logger.info({language, voice}, `Task:say - using vendor: ${vendor}`);
|
||||
this.logger.info({vendor, language, voice}, 'TaskSay:exec');
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
@@ -44,6 +48,7 @@ class TaskSay extends Task {
|
||||
// synthesize all of the text elements
|
||||
let lastUpdated = false;
|
||||
const filepath = (await Promise.all(this.text.map(async(text) => {
|
||||
if (text.startsWith('silence_stream://')) return text;
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
text,
|
||||
vendor,
|
||||
@@ -80,7 +85,11 @@ class TaskSay extends Task {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
||||
}
|
||||
else await ep.play(filepath[segment]);
|
||||
else {
|
||||
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
|
||||
await ep.play(filepath[segment]);
|
||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||
}
|
||||
} while (!this.killed && ++segment < filepath.length);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -65,7 +65,7 @@ class TaskSipRefer extends Task {
|
||||
const status = arr[1];
|
||||
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
||||
if (this.eventHook) {
|
||||
await cs.requestor.request(this.eventHook, {event: 'transfer-status', call_status: status});
|
||||
await cs.requestor.request('verb:hook', this.eventHook, {event: 'transfer-status', call_status: status});
|
||||
}
|
||||
if (status >= 200) {
|
||||
await this.performAction({refer_status: 202, final_referred_call_status: status});
|
||||
|
||||
@@ -98,8 +98,16 @@
|
||||
"finishOnKey": "string",
|
||||
"input": "array",
|
||||
"numDigits": "number",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"partialResultHook": "object|string",
|
||||
"speechTimeout": "number",
|
||||
"listenDuringPrompt": "boolean",
|
||||
"listenAfterSpeech": "boolean",
|
||||
"dtmfBargein": "boolean",
|
||||
"bargein": "boolean",
|
||||
"minBargeinWordCount": "number",
|
||||
"timeout": "number",
|
||||
"recognizer": "#recognizer",
|
||||
"play": "#play",
|
||||
@@ -338,6 +346,7 @@
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"headers": "object",
|
||||
"name": "string",
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
@@ -389,6 +398,7 @@
|
||||
"enum": ["google", "aws", "microsoft", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"vad": "#vad",
|
||||
"hints": "array",
|
||||
"altLanguages": "array",
|
||||
"profanityFilter": "boolean",
|
||||
@@ -457,5 +467,15 @@
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"vad": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"voiceMs": "number",
|
||||
"mode": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,9 @@ class Task extends Emitter {
|
||||
kill(cs) {
|
||||
if (this.cs && !this.cs.isConfirmCallSession) this.logger.debug(`${this.name} is being killed`);
|
||||
this._killInProgress = true;
|
||||
// no-op
|
||||
|
||||
/* remove reference to parent task or else entangled parent-child tasks will not be gc'ed */
|
||||
setImmediate(() => this.parentTask = null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +107,7 @@ class Task extends Emitter {
|
||||
async performAction(results, expectResponse = true) {
|
||||
if (this.actionHook) {
|
||||
const params = results ? Object.assign(results, this.cs.callInfo.toJSON()) : this.cs.callInfo.toJSON();
|
||||
const json = await this.cs.requestor.request(this.actionHook, params);
|
||||
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params);
|
||||
if (expectResponse && json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
@@ -118,7 +120,7 @@ class Task extends Emitter {
|
||||
}
|
||||
|
||||
async performHook(cs, hook, results) {
|
||||
const json = await cs.requestor.request(hook, results);
|
||||
const json = await cs.requestor.request('verb:hook', hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -22,6 +22,10 @@ class TaskTranscribe extends Task {
|
||||
this.interim = !!recognizer.interim;
|
||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
|
||||
/* google-specific options */
|
||||
this.hints = recognizer.hints || [];
|
||||
this.profanityFilter = recognizer.profanityFilter;
|
||||
@@ -105,6 +109,12 @@ class TaskTranscribe extends Task {
|
||||
async _startTranscribing(cs, ep) {
|
||||
const opts = {};
|
||||
|
||||
if (this.vad.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||
@@ -236,7 +246,7 @@ class TaskTranscribe extends Task {
|
||||
}
|
||||
|
||||
if (this.transcriptionHook) {
|
||||
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
this.cs.requestor.request('verb:hook', this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
|
||||
}
|
||||
if (this.parentTask) {
|
||||
|
||||
@@ -45,6 +45,7 @@ class SnsNotifier extends Emitter {
|
||||
}, 'response from SNS SubscribeURL');
|
||||
const data = await this.describeInstance();
|
||||
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState;
|
||||
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
||||
break;
|
||||
|
||||
case 'Notification':
|
||||
|
||||
75
lib/utils/base-requestor.js
Normal file
75
lib/utils/base-requestor.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const assert = require('assert');
|
||||
const Emitter = require('events');
|
||||
const crypto = require('crypto');
|
||||
const timeSeries = require('@jambonz/time-series');
|
||||
let alerter ;
|
||||
|
||||
class BaseRequestor extends Emitter {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super();
|
||||
assert(typeof hook === 'object');
|
||||
|
||||
this.logger = logger;
|
||||
this.url = hook.url;
|
||||
|
||||
this.username = hook.username;
|
||||
this.password = hook.password;
|
||||
this.secret = secret;
|
||||
this.account_sid = account_sid;
|
||||
|
||||
const {stats} = require('../../').srf.locals;
|
||||
this.stats = stats;
|
||||
|
||||
if (!alerter) {
|
||||
alerter = timeSeries(logger, {
|
||||
host: process.env.JAMBONES_TIME_SERIES_HOST,
|
||||
commitSize: 50,
|
||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get Alerter() {
|
||||
return alerter;
|
||||
}
|
||||
|
||||
close() {
|
||||
/* subclass responsibility */
|
||||
}
|
||||
|
||||
_computeSignature(payload, timestamp, secret) {
|
||||
assert(secret);
|
||||
const data = `${timestamp}.${JSON.stringify(payload)}`;
|
||||
return crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(data, 'utf8')
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
_generateSigHeader(payload, secret) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const signature = this._computeSignature(payload, timestamp, secret);
|
||||
const scheme = 'v1';
|
||||
return {
|
||||
'Jambonz-Signature': `t=${timestamp},${scheme}=${signature}`
|
||||
};
|
||||
}
|
||||
|
||||
_isAbsoluteUrl(u) {
|
||||
return typeof u === 'string' &&
|
||||
u.startsWith('https://') || u.startsWith('http://') ||
|
||||
u.startsWith('ws://') || u.startsWith('wss://');
|
||||
}
|
||||
_isRelativeUrl(u) {
|
||||
return typeof u === 'string' && u.startsWith('/');
|
||||
}
|
||||
_roundTrip(startAt) {
|
||||
const diff = process.hrtime(startAt);
|
||||
const time = diff[0] * 1e3 + diff[1] * 1e-6;
|
||||
return time.toFixed(0);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = BaseRequestor;
|
||||
@@ -105,6 +105,15 @@
|
||||
"Hangup": "hangup",
|
||||
"Replaced": "replaced"
|
||||
},
|
||||
"HookMsgTypes": [
|
||||
"session:new",
|
||||
"session:reconnect",
|
||||
"session:redirect",
|
||||
"call:status",
|
||||
"queue:status",
|
||||
"verb:hook",
|
||||
"jambonz:error"
|
||||
],
|
||||
"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"
|
||||
|
||||
@@ -36,6 +36,10 @@ const speechMapper = (cred) => {
|
||||
obj.api_key = o.api_key;
|
||||
obj.region = o.region;
|
||||
}
|
||||
else if ('wellsaid' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
@@ -53,6 +57,7 @@ module.exports = (logger, srf) => {
|
||||
const haveGoogle = speech.find((s) => s.vendor === 'google');
|
||||
const haveAws = speech.find((s) => s.vendor === 'aws');
|
||||
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
|
||||
const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
|
||||
if (!haveGoogle || !haveAws || !haveMicrosoft) {
|
||||
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
|
||||
if (r3.length) {
|
||||
@@ -68,6 +73,10 @@ module.exports = (logger, srf) => {
|
||||
const ms = r3.find((s) => s.vendor === 'microsoft');
|
||||
if (ms) speech.push(speechMapper(ms));
|
||||
}
|
||||
if (!haveWellsaid) {
|
||||
const wellsaid = r3.find((s) => s.vendor === 'wellsaid');
|
||||
if (wellsaid) speech.push(speechMapper(wellsaid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
105
lib/utils/http-requestor.js
Normal file
105
lib/utils/http-requestor.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const bent = require('bent');
|
||||
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 toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
|
||||
function basicAuth(username, password) {
|
||||
if (!username || !password) return {};
|
||||
const creds = `${username}:${password || ''}`;
|
||||
const header = `Basic ${toBase64(creds)}`;
|
||||
return {Authorization: header};
|
||||
}
|
||||
|
||||
|
||||
class HttpRequestor extends BaseRequestor {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super(logger, account_sid, hook, secret);
|
||||
|
||||
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);
|
||||
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
assert(['GET', 'POST'].includes(this.method));
|
||||
}
|
||||
|
||||
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(type, hook, params) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
const url = hook.url || hook;
|
||||
const method = hook.method || 'POST';
|
||||
|
||||
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;
|
||||
try {
|
||||
const sigHeader = this._generateSigHeader(payload, this.secret);
|
||||
const headers = {...sigHeader, ...this.authHeader};
|
||||
//this.logger.info({url, headers}, 'send webhook');
|
||||
buf = this._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: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
|
||||
}
|
||||
else if (err.name === 'StatusError') {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_STATUS_FAILURE, url, status: err.statusCode};
|
||||
}
|
||||
else {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url, detail: err.message};
|
||||
}
|
||||
this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
|
||||
|
||||
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()}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HttpRequestor;
|
||||
@@ -8,6 +8,7 @@ const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const AdultingCallSession = require('../session/adulting-call-session');
|
||||
const deepcopy = require('deepcopy');
|
||||
const moment = require('moment');
|
||||
const stripCodecs = require('./strip-ancillary-codecs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
@@ -59,10 +60,16 @@ class SingleDialer extends Emitter {
|
||||
opts.headers = opts.headers || {};
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
...(this.target.headers || {}),
|
||||
'X-Jambonz-Routing': this.target.type,
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
'X-Call-Sid': this.callSid
|
||||
};
|
||||
if (srf.locals.fsUUID) {
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
};
|
||||
}
|
||||
this.ms = ms;
|
||||
let uri, to;
|
||||
try {
|
||||
@@ -143,6 +150,7 @@ class SingleDialer extends Emitter {
|
||||
* (a) create a CallInfo for this call
|
||||
* (a) create a logger for this call
|
||||
*/
|
||||
req.srf = srf;
|
||||
this.callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
parentCallInfo: this.parentCallInfo,
|
||||
@@ -214,6 +222,7 @@ class SingleDialer extends Emitter {
|
||||
if (this.confirmHook) this._executeApp(this.confirmHook);
|
||||
else this.emit('accept');
|
||||
} catch (err) {
|
||||
this.inviteInProgress = null;
|
||||
const status = {callStatus: CallStatus.Failed};
|
||||
if (err instanceof SipError) {
|
||||
status.sipStatus = err.status;
|
||||
@@ -316,9 +325,10 @@ class SingleDialer extends Emitter {
|
||||
return cs;
|
||||
}
|
||||
|
||||
async releaseMediaToSBC(remoteSdp) {
|
||||
async releaseMediaToSBC(remoteSdp, localSdp) {
|
||||
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
|
||||
await this.dlg.modify(remoteSdp, {
|
||||
const sdp = stripCodecs(this.logger, remoteSdp, localSdp) || remoteSdp;
|
||||
await this.dlg.modify(sdp, {
|
||||
headers: {
|
||||
'X-Reason': 'release-media'
|
||||
}
|
||||
@@ -347,7 +357,7 @@ class SingleDialer extends Emitter {
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.requestor.request(this.application.call_status_hook, this.callInfo.toJSON());
|
||||
this.requestor.request('call:status', this.application.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
this.logger.info(err, `SingleDialer:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ module.exports = (logger) => {
|
||||
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
}
|
||||
else if (process.env.K8S && process.env.K8S_SBC_SIP_SERVICE_NAME) {
|
||||
sbcs = [`${process.env.K8S_SBC_SIP_SERVICE_NAME}:5060`];
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
}
|
||||
|
||||
// listen for SNS lifecycle changes
|
||||
let lifecycleEmitter = new Emitter();
|
||||
@@ -28,6 +32,10 @@ module.exports = (logger) => {
|
||||
lifecycleEmitter = await require('./aws-sns-lifecycle')(logger);
|
||||
|
||||
lifecycleEmitter
|
||||
.on('SubscriptionConfirmation', ({publicIp}) => {
|
||||
const {srf} = require('../..');
|
||||
srf.locals.publicIp = publicIp;
|
||||
})
|
||||
.on(LifeCycleEvents.ScaleIn, () => {
|
||||
logger.info('AWS scale-in notification: begin drying up calls');
|
||||
dryUpCalls = true;
|
||||
|
||||
30
lib/utils/strip-ancillary-codecs.js
Normal file
30
lib/utils/strip-ancillary-codecs.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const sdpTransform = require('sdp-transform');
|
||||
|
||||
const stripCodecs = (logger, remoteSdp, localSdp) => {
|
||||
try {
|
||||
const sdp = sdpTransform.parse(remoteSdp);
|
||||
const local = sdpTransform.parse(localSdp);
|
||||
const m = local.media
|
||||
.find((m) => 'audio' === m.type);
|
||||
const pt = m.rtp[0].payload;
|
||||
|
||||
/* manipulate on the audio section */
|
||||
const audio = sdp.media.find((m) => 'audio' === m.type);
|
||||
|
||||
/* discard all of the codecs except the first in our 200 OK, and telephony-events */
|
||||
const ptSaves = audio.rtp
|
||||
.filter((r) => r.codec === 'telephone-event' || r.payload === pt)
|
||||
.map((r) => r.payload);
|
||||
const rtp = audio.rtp.filter((r) => ptSaves.includes(r.payload));
|
||||
|
||||
/* reattach the new rtp sections and stripped payload list */
|
||||
audio.rtp = rtp;
|
||||
audio.payloads = rtp.map((r) => r.payload).join(' ');
|
||||
return sdpTransform.write(sdp);
|
||||
} catch (err) {
|
||||
logger.error({err, remoteSdp, localSdp}, 'strip-ancillary-codecs error');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = stripCodecs;
|
||||
|
||||
249
lib/utils/ws-requestor.js
Normal file
249
lib/utils/ws-requestor.js
Normal file
@@ -0,0 +1,249 @@
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const short = require('short-uuid');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const Websocket = require('ws');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const HttpRequestor = require('./http-requestor');
|
||||
const MAX_RECONNECTS = 5;
|
||||
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
|
||||
|
||||
class WsRequestor extends BaseRequestor {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super(logger, account_sid, hook, secret);
|
||||
this.connections = 0;
|
||||
this.messagesInFlight = new Map();
|
||||
this.maliciousClient = false;
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
|
||||
this.on('socket-closed', this._onSocketClosed.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON payload over the websocket. If this is the first request,
|
||||
* open the websocket.
|
||||
* All requests expect an ack message in response
|
||||
* @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(type, hook, params) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
const url = hook.url || hook;
|
||||
|
||||
if (this.maliciousClient) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
|
||||
return;
|
||||
}
|
||||
|
||||
/* if we have an absolute url, and it is http then do a standard webhook */
|
||||
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
|
||||
this.logger.debug({hook}, 'WsRequestor: sending a webhook');
|
||||
const requestor = new HttpRequestor(this.logger, this.account_sid, hook, this.secret);
|
||||
return requestor.request(type, hook, params);
|
||||
}
|
||||
|
||||
/* connect if necessary */
|
||||
if (!this.ws) {
|
||||
if (this.connections >= MAX_RECONNECTS) {
|
||||
throw new Error(`max attempts connecting to ${this.url}`);
|
||||
}
|
||||
try {
|
||||
const startAt = process.hrtime();
|
||||
await this._connect();
|
||||
const rtt = this._roundTrip(startAt);
|
||||
this.stats.histogram('app.hook.connect_time', rtt, ['hook_type:app']);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'WsRequestor:request - failed connecting');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
assert(this.ws);
|
||||
|
||||
/* prepare and send message */
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||
|
||||
const msgid = short.generate();
|
||||
const obj = {
|
||||
type,
|
||||
msgid,
|
||||
hook: type === 'verb:hook' ? url : undefined,
|
||||
data: {...payload}
|
||||
};
|
||||
|
||||
this.logger.debug({obj}, `WsRequestor:request ${url}`);
|
||||
|
||||
/* simple notifications */
|
||||
if (['call:status', 'jambonz:error'].includes(type)) {
|
||||
this.ws.send(JSON.stringify(obj));
|
||||
return;
|
||||
}
|
||||
|
||||
/* messages that require an ack */
|
||||
return new Promise((resolve, reject) => {
|
||||
/* give the far end a reasonable amount of time to ack our message */
|
||||
const timer = setTimeout(() => {
|
||||
const {failure} = this.messagesInFlight.get(msgid);
|
||||
failure && failure(`timeout from far end for msgid ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
}, RESPONSE_TIMEOUT_MS);
|
||||
|
||||
/* save the message info for reply */
|
||||
const startAt = process.hrtime();
|
||||
this.messagesInFlight.set(msgid, {
|
||||
success: (response) => {
|
||||
clearTimeout(timer);
|
||||
const rtt = this._roundTrip(startAt);
|
||||
this.logger.info({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
|
||||
this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']);
|
||||
resolve(response);
|
||||
},
|
||||
failure: (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
/* send the message */
|
||||
this.ws.send(JSON.stringify(obj));
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.logger.info('WsRequestor: closing socket');
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
_connect() {
|
||||
assert(!this.ws);
|
||||
return new Promise((resolve, reject) => {
|
||||
let opts = {
|
||||
followRedirects: true,
|
||||
maxRedirects: 2,
|
||||
handshakeTimeout: 1000,
|
||||
maxPayload: 8096,
|
||||
};
|
||||
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
|
||||
|
||||
this
|
||||
.once('ready', (ws) => {
|
||||
this.ws = ws;
|
||||
this.removeAllListeners('not-ready');
|
||||
resolve();
|
||||
})
|
||||
.once('not-ready', () => {
|
||||
this.removeAllListeners('ready');
|
||||
reject();
|
||||
});
|
||||
const ws = new Websocket(this.url, ['ws.jambonz.org'], opts);
|
||||
this._setHandlers(ws);
|
||||
});
|
||||
}
|
||||
|
||||
_setHandlers(ws) {
|
||||
ws
|
||||
.on('open', this._onOpen.bind(this, ws))
|
||||
.on('close', this._onClose.bind(this))
|
||||
.on('message', this._onMessage.bind(this))
|
||||
.on('unexpected-response', this._onUnexpectedResponse.bind(this, ws))
|
||||
.on('error', this._onError.bind(this));
|
||||
}
|
||||
|
||||
_onError(err) {
|
||||
this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
|
||||
if (this.connections > 0) this.emit('socket-closed');
|
||||
this.emit('not-ready');
|
||||
}
|
||||
|
||||
_onOpen(ws) {
|
||||
assert(!this.ws);
|
||||
this.emit('ready', ws);
|
||||
this.logger.info({url: this.url}, 'WsRequestor - successfully connected');
|
||||
}
|
||||
|
||||
_onClose() {
|
||||
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
||||
this.emit('socket-closed');
|
||||
}
|
||||
|
||||
_onUnexpectedResponse(ws, req, res) {
|
||||
assert(!this.ws);
|
||||
this.logger.info({
|
||||
headers: res.headers,
|
||||
statusCode: res.statusCode,
|
||||
statusMessage: res.statusMessage
|
||||
}, 'WsRequestor - unexpected response');
|
||||
this.emit('connection-failure');
|
||||
this.emit('not-ready');
|
||||
}
|
||||
|
||||
_onSocketClosed() {
|
||||
this.ws = null;
|
||||
if (this.connections > 0) {
|
||||
if (++this.connections < MAX_RECONNECTS) {
|
||||
setImmediate(this.connect.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onMessage(content, isBinary) {
|
||||
if (this.isBinary) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:_onMessage - discarding binary message');
|
||||
this.maliciousClient = true;
|
||||
this.ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
/* messages must be JSON format */
|
||||
try {
|
||||
const {type, msgid, command, queueCommand = false, data} = JSON.parse(content);
|
||||
assert.ok(type, 'type property not supplied');
|
||||
|
||||
switch (type) {
|
||||
case 'ack':
|
||||
assert.ok(msgid, 'msgid not supplied');
|
||||
this._recvAck(msgid, data);
|
||||
break;
|
||||
|
||||
case 'command':
|
||||
assert.ok(command, 'command property not supplied');
|
||||
assert.ok(data, 'data property not supplied');
|
||||
this._recvCommand(msgid, command, queueCommand, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert.ok(false, `invalid type property: ${type}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'WsRequestor:_onMessage - invalid incoming message');
|
||||
}
|
||||
}
|
||||
|
||||
_recvAck(msgid, data) {
|
||||
const obj = this.messagesInFlight.get(msgid);
|
||||
if (!obj) {
|
||||
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
const {success} = obj;
|
||||
success && success(data);
|
||||
}
|
||||
|
||||
_recvCommand(msgid, command, queueCommand, data) {
|
||||
// TODO: validate command
|
||||
this.logger.info({msgid, command, queueCommand, data}, 'received command');
|
||||
this.emit('command', {msgid, command, queueCommand, data});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WsRequestor;
|
||||
320
package-lock.json
generated
320
package-lock.json
generated
@@ -1,35 +1,39 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.1",
|
||||
"version": "v0.7.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "v0.7.1",
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cognigy/socket-client": "^4.5.5",
|
||||
"@jambonz/db-helpers": "^0.6.16",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.15",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.26",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"aws-sdk": "^2.1073.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.2",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^2.0.13",
|
||||
"drachtio-srf": "^4.4.55",
|
||||
"drachtio-srf": "^4.4.61",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.13.2",
|
||||
"pino": "^6.13.4",
|
||||
"short-uuid": "^4.2.0",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
"ws": "^8.5.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -42,6 +46,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
"utf-8-validate": "^5.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -544,15 +552,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/realtimedb-helpers": {
|
||||
"version": "0.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.15.tgz",
|
||||
"integrity": "sha512-KQ1S8gj36G0JYlwJ+2NF5pteDzv5wKaRqx6U+eIYx4+z5rtUe0iMOhJx6I38LhUnC965dws0FuZWvGQ/YUFilg==",
|
||||
"version": "0.4.26",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.26.tgz",
|
||||
"integrity": "sha512-vGNC5IsYj7Qj7mOfgfti0/ZouYs+9GHEX4v7w3JguPW1NqUbvFayPs3xo49di3NJsHV0NnKWx2rMgrZzvWTgTA==",
|
||||
"dependencies": {
|
||||
"@google-cloud/text-to-speech": "^3.4.0",
|
||||
"@jambonz/promisify-redis": "^0.0.6",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"aws-sdk": "^2.1072.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.3",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.19.0",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.20.0",
|
||||
"redis": "^3.1.2"
|
||||
}
|
||||
},
|
||||
@@ -566,9 +575,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/time-series": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.5.tgz",
|
||||
"integrity": "sha512-JDkAshqq9VRdIY7S/Edz9iIyKNoyCoUMfn0IUVyUG4Hf53fmhYRCnFnDTdMFAs6Y4amE39m3968nrZRr+lVIqQ==",
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.6.tgz",
|
||||
"integrity": "sha512-EFO1ZFUazHLCzDYkAbpke0IfmPRogLkGg+KGWwz3SGpnuiltfohQ3TsJ5KG8Gp9mXqdpkyAioWSdaJxGDquthQ==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"influx": "^5.8.0"
|
||||
@@ -905,14 +914,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-sdk": {
|
||||
"version": "2.1060.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1060.0.tgz",
|
||||
"integrity": "sha512-c734/CZiVSsuVnEkx/7dodI8ndgOnxiCTwwlEFlMxdAZfLLJplteFwi6c/J2GZQktvz2ysV/HVtNKcJkasYJzw==",
|
||||
"version": "2.1073.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1073.0.tgz",
|
||||
"integrity": "sha512-TtyHDL4ZEs8Zh/DqWY/hv745DTWrIwOyBAvfjBJ45RE9h0TjpWqCIowEtb6gRPAKyPPyfGH4s+rEYu07vNK1Hg==",
|
||||
"dependencies": {
|
||||
"buffer": "4.9.2",
|
||||
"events": "1.1.1",
|
||||
"ieee754": "1.1.13",
|
||||
"jmespath": "0.15.0",
|
||||
"jmespath": "0.16.0",
|
||||
"querystring": "0.2.0",
|
||||
"sax": "1.2.1",
|
||||
"url": "0.10.3",
|
||||
@@ -1096,6 +1105,19 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"node_modules/bufferutil": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz",
|
||||
"integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
|
||||
@@ -1601,9 +1623,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-sip": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-sip/-/drachtio-sip-0.6.2.tgz",
|
||||
"integrity": "sha512-BkiRZq3Yq2WVSGY3M7Hv4yX4dIW/o0/4xNMcm26IxT71YIRy07UtbQUHaMI3P2HfPu5zK6RoQW2MHrxPXtz6ZQ==",
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-sip/-/drachtio-sip-0.6.3.tgz",
|
||||
"integrity": "sha512-sQQ2JdRQ58z+WbkxORlWKZCeinu499tugfsdh7jRBWX6SO4odDevT5JL+42dMlwrL83CZUL6w2Xjgu0uaABoMQ==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
@@ -1611,9 +1633,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-sip/node_modules/eslint-plugin-promise": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.1.tgz",
|
||||
"integrity": "sha512-XgdcdyNzHfmlQyweOPTxmc7pIsS6dE4MvwhXWMQ2Dxs1XAL2GJDilUsjWen6TWik0aSI+zD/PqocZBblcm9rdA==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz",
|
||||
"integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==",
|
||||
"engines": {
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
},
|
||||
@@ -1622,15 +1644,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-srf": {
|
||||
"version": "4.4.59",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.4.59.tgz",
|
||||
"integrity": "sha512-hrW9bZ8TZR9JQ3pqI+nyrI1eAzEOwHuvm1lNL1fbmZmRddKJzYdylkgVoyURs/OlT/nANy/M43GrQjcGP4psPw==",
|
||||
"version": "4.4.61",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.4.61.tgz",
|
||||
"integrity": "sha512-HZwKTAR6V7dc84BV50h5xAopkQ1k8wUy04JXDmq2/O+uryANFwaklICidg1oJkIrnYUPWQn/1IfZnqGSv9O2Sw==",
|
||||
"dependencies": {
|
||||
"async": "^1.4.2",
|
||||
"debug": "^3.2.7",
|
||||
"delegates": "^0.1.0",
|
||||
"deprecate": "^1.1.1",
|
||||
"drachtio-sip": "^0.6.2",
|
||||
"drachtio-sip": "^0.6.3",
|
||||
"node-noop": "0.0.1",
|
||||
"only": "0.0.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
@@ -1638,7 +1660,7 @@
|
||||
"sip-methods": "^0.3.0",
|
||||
"sip-status": "^0.1.0",
|
||||
"utils-merge": "1.0.0",
|
||||
"uuid": "^3.4.0"
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.9.3"
|
||||
@@ -1657,15 +1679,6 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-srf/node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexify": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
|
||||
@@ -2257,11 +2270,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig=="
|
||||
},
|
||||
"node_modules/fastify-warning": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
|
||||
"integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw=="
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
|
||||
@@ -2717,6 +2725,14 @@
|
||||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz",
|
||||
"integrity": "sha1-B8tKXfJa3Z6Cbrxn3Mn9idsn2Eo="
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
|
||||
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hot-shots": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-8.5.2.tgz",
|
||||
@@ -3309,9 +3325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jmespath": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
|
||||
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=",
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
|
||||
"integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@@ -3509,9 +3525,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/microsoft-cognitiveservices-speech-sdk": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.19.0.tgz",
|
||||
"integrity": "sha512-FYyXdJcTLX8Iunnn9XHzTfR+43YdEZn1q89KX6Vzqtaa4hz/LemdC9Zni50VIfkXMNtq2sJFYf5wKXvxm1wQHA==",
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.20.0.tgz",
|
||||
"integrity": "sha512-/KWNHQb36nhIkBr6HClh+T+gHicgmC5JKJhCnQmL6XGAAndLtew0aRbh7cMK10IFNTgozGKiejhrfnwiYH3IBA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^6.0.1",
|
||||
@@ -3523,7 +3539,7 @@
|
||||
"simple-lru-cache": "0.0.2",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^8.3.0",
|
||||
"ws": "^7.3.1"
|
||||
"ws": "^7.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/microsoft-cognitiveservices-speech-sdk/node_modules/https-proxy-agent": {
|
||||
@@ -3546,6 +3562,26 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/microsoft-cognitiveservices-speech-sdk/node_modules/ws": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
@@ -3729,6 +3765,17 @@
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
|
||||
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-noop": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-noop/-/node-noop-0.0.1.tgz",
|
||||
@@ -4045,15 +4092,15 @@
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz",
|
||||
"integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==",
|
||||
"version": "6.13.4",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.4.tgz",
|
||||
"integrity": "sha512-g4tHSISmQJYUEKEMVdaZ+ZokWwFnTwZL5JPn+lnBVZ1BuBbrSchrXwQINknkM5+Q4fF6U9NjiI8PWwwMDHt9zA==",
|
||||
"dependencies": {
|
||||
"fast-redact": "^3.0.0",
|
||||
"fast-safe-stringify": "^2.0.8",
|
||||
"fastify-warning": "^0.2.0",
|
||||
"flatstr": "^1.0.12",
|
||||
"pino-std-serializers": "^3.1.0",
|
||||
"process-warning": "^1.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"sonic-boom": "^1.0.2"
|
||||
},
|
||||
@@ -4098,6 +4145,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
|
||||
"integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -5112,9 +5164,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
@@ -5125,6 +5177,19 @@
|
||||
"resolved": "https://registry.npmjs.org/username-sync/-/username-sync-1.0.3.tgz",
|
||||
"integrity": "sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA=="
|
||||
},
|
||||
"node_modules/utf-8-validate": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.8.tgz",
|
||||
"integrity": "sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -5341,11 +5406,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
|
||||
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
@@ -5874,15 +5939,16 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/realtimedb-helpers": {
|
||||
"version": "0.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.15.tgz",
|
||||
"integrity": "sha512-KQ1S8gj36G0JYlwJ+2NF5pteDzv5wKaRqx6U+eIYx4+z5rtUe0iMOhJx6I38LhUnC965dws0FuZWvGQ/YUFilg==",
|
||||
"version": "0.4.26",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.26.tgz",
|
||||
"integrity": "sha512-vGNC5IsYj7Qj7mOfgfti0/ZouYs+9GHEX4v7w3JguPW1NqUbvFayPs3xo49di3NJsHV0NnKWx2rMgrZzvWTgTA==",
|
||||
"requires": {
|
||||
"@google-cloud/text-to-speech": "^3.4.0",
|
||||
"@jambonz/promisify-redis": "^0.0.6",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"aws-sdk": "^2.1072.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.3",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.19.0",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.20.0",
|
||||
"redis": "^3.1.2"
|
||||
}
|
||||
},
|
||||
@@ -5896,9 +5962,9 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/time-series": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.5.tgz",
|
||||
"integrity": "sha512-JDkAshqq9VRdIY7S/Edz9iIyKNoyCoUMfn0IUVyUG4Hf53fmhYRCnFnDTdMFAs6Y4amE39m3968nrZRr+lVIqQ==",
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.6.tgz",
|
||||
"integrity": "sha512-EFO1ZFUazHLCzDYkAbpke0IfmPRogLkGg+KGWwz3SGpnuiltfohQ3TsJ5KG8Gp9mXqdpkyAioWSdaJxGDquthQ==",
|
||||
"requires": {
|
||||
"debug": "^4.3.1",
|
||||
"influx": "^5.8.0"
|
||||
@@ -6175,14 +6241,14 @@
|
||||
}
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.1060.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1060.0.tgz",
|
||||
"integrity": "sha512-c734/CZiVSsuVnEkx/7dodI8ndgOnxiCTwwlEFlMxdAZfLLJplteFwi6c/J2GZQktvz2ysV/HVtNKcJkasYJzw==",
|
||||
"version": "2.1073.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1073.0.tgz",
|
||||
"integrity": "sha512-TtyHDL4ZEs8Zh/DqWY/hv745DTWrIwOyBAvfjBJ45RE9h0TjpWqCIowEtb6gRPAKyPPyfGH4s+rEYu07vNK1Hg==",
|
||||
"requires": {
|
||||
"buffer": "4.9.2",
|
||||
"events": "1.1.1",
|
||||
"ieee754": "1.1.13",
|
||||
"jmespath": "0.15.0",
|
||||
"jmespath": "0.16.0",
|
||||
"querystring": "0.2.0",
|
||||
"sax": "1.2.1",
|
||||
"url": "0.10.3",
|
||||
@@ -6331,6 +6397,15 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"bufferutil": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz",
|
||||
"integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
|
||||
@@ -6743,9 +6818,9 @@
|
||||
}
|
||||
},
|
||||
"drachtio-sip": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-sip/-/drachtio-sip-0.6.2.tgz",
|
||||
"integrity": "sha512-BkiRZq3Yq2WVSGY3M7Hv4yX4dIW/o0/4xNMcm26IxT71YIRy07UtbQUHaMI3P2HfPu5zK6RoQW2MHrxPXtz6ZQ==",
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-sip/-/drachtio-sip-0.6.3.tgz",
|
||||
"integrity": "sha512-sQQ2JdRQ58z+WbkxORlWKZCeinu499tugfsdh7jRBWX6SO4odDevT5JL+42dMlwrL83CZUL6w2Xjgu0uaABoMQ==",
|
||||
"requires": {
|
||||
"debug": "^4.3.1",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
@@ -6753,23 +6828,23 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-plugin-promise": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.1.tgz",
|
||||
"integrity": "sha512-XgdcdyNzHfmlQyweOPTxmc7pIsS6dE4MvwhXWMQ2Dxs1XAL2GJDilUsjWen6TWik0aSI+zD/PqocZBblcm9rdA==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz",
|
||||
"integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"drachtio-srf": {
|
||||
"version": "4.4.59",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.4.59.tgz",
|
||||
"integrity": "sha512-hrW9bZ8TZR9JQ3pqI+nyrI1eAzEOwHuvm1lNL1fbmZmRddKJzYdylkgVoyURs/OlT/nANy/M43GrQjcGP4psPw==",
|
||||
"version": "4.4.61",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.4.61.tgz",
|
||||
"integrity": "sha512-HZwKTAR6V7dc84BV50h5xAopkQ1k8wUy04JXDmq2/O+uryANFwaklICidg1oJkIrnYUPWQn/1IfZnqGSv9O2Sw==",
|
||||
"requires": {
|
||||
"async": "^1.4.2",
|
||||
"debug": "^3.2.7",
|
||||
"delegates": "^0.1.0",
|
||||
"deprecate": "^1.1.1",
|
||||
"drachtio-sip": "^0.6.2",
|
||||
"drachtio-sip": "^0.6.3",
|
||||
"node-noop": "0.0.1",
|
||||
"only": "0.0.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
@@ -6777,7 +6852,7 @@
|
||||
"sip-methods": "^0.3.0",
|
||||
"sip-status": "^0.1.0",
|
||||
"utils-merge": "1.0.0",
|
||||
"uuid": "^3.4.0"
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
@@ -6792,11 +6867,6 @@
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7269,11 +7339,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig=="
|
||||
},
|
||||
"fastify-warning": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
|
||||
"integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw=="
|
||||
},
|
||||
"file-entry-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
|
||||
@@ -7625,6 +7690,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"helmet": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
|
||||
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg=="
|
||||
},
|
||||
"hot-shots": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-8.5.2.tgz",
|
||||
@@ -8056,9 +8126,9 @@
|
||||
}
|
||||
},
|
||||
"jmespath": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
|
||||
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
|
||||
"integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
@@ -8214,9 +8284,9 @@
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"microsoft-cognitiveservices-speech-sdk": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.19.0.tgz",
|
||||
"integrity": "sha512-FYyXdJcTLX8Iunnn9XHzTfR+43YdEZn1q89KX6Vzqtaa4hz/LemdC9Zni50VIfkXMNtq2sJFYf5wKXvxm1wQHA==",
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.20.0.tgz",
|
||||
"integrity": "sha512-/KWNHQb36nhIkBr6HClh+T+gHicgmC5JKJhCnQmL6XGAAndLtew0aRbh7cMK10IFNTgozGKiejhrfnwiYH3IBA==",
|
||||
"requires": {
|
||||
"agent-base": "^6.0.1",
|
||||
"asn1.js-rfc2560": "^5.0.1",
|
||||
@@ -8227,7 +8297,7 @@
|
||||
"simple-lru-cache": "0.0.2",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^8.3.0",
|
||||
"ws": "^7.3.1"
|
||||
"ws": "^7.5.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"https-proxy-agent": {
|
||||
@@ -8245,6 +8315,12 @@
|
||||
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8385,6 +8461,12 @@
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w=="
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
|
||||
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==",
|
||||
"optional": true
|
||||
},
|
||||
"node-noop": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-noop/-/node-noop-0.0.1.tgz",
|
||||
@@ -8629,15 +8711,15 @@
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"pino": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz",
|
||||
"integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==",
|
||||
"version": "6.13.4",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.4.tgz",
|
||||
"integrity": "sha512-g4tHSISmQJYUEKEMVdaZ+ZokWwFnTwZL5JPn+lnBVZ1BuBbrSchrXwQINknkM5+Q4fF6U9NjiI8PWwwMDHt9zA==",
|
||||
"requires": {
|
||||
"fast-redact": "^3.0.0",
|
||||
"fast-safe-stringify": "^2.0.8",
|
||||
"fastify-warning": "^0.2.0",
|
||||
"flatstr": "^1.0.12",
|
||||
"pino-std-serializers": "^3.1.0",
|
||||
"process-warning": "^1.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"sonic-boom": "^1.0.2"
|
||||
}
|
||||
@@ -8670,6 +8752,11 @@
|
||||
"fromentries": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"process-warning": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
|
||||
"integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -9473,9 +9560,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
@@ -9486,6 +9573,15 @@
|
||||
"resolved": "https://registry.npmjs.org/username-sync/-/username-sync-1.0.3.tgz",
|
||||
"integrity": "sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA=="
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.8.tgz",
|
||||
"integrity": "sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -9659,9 +9755,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
|
||||
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml2js": {
|
||||
|
||||
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.1",
|
||||
"version": "v0.7.3",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -30,24 +30,27 @@
|
||||
"@jambonz/db-helpers": "^0.6.16",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.16",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.26",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"aws-sdk": "^2.1073.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.2",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^2.0.13",
|
||||
"drachtio-srf": "^4.4.55",
|
||||
"drachtio-srf": "^4.4.61",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.13.2",
|
||||
"pino": "^6.13.4",
|
||||
"short-uuid": "^4.2.0",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
"ws": "^8.5.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -57,5 +60,9 @@
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
"utf-8-validate": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user