mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-19 04:17:44 +00:00
added support for conference verb
This commit is contained in:
4
app.js
4
app.js
@@ -42,7 +42,9 @@ const InboundCallSession = require('./lib/session/inbound-call-session');
|
||||
if (process.env.DRACHTIO_HOST) {
|
||||
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
|
||||
srf.on('connect', (err, hp) => {
|
||||
logger.info(`connected to drachtio listening on ${hp}`);
|
||||
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
|
||||
srf.locals.localSipAddress = `${arr[2]}`;
|
||||
logger.info(`connected to drachtio listening on ${hp}, local sip address is ${srf.locals.localSipAddress}`);
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -2,6 +2,7 @@ const api = require('express').Router();
|
||||
|
||||
api.use('/createCall', require('./create-call'));
|
||||
api.use('/updateCall', require('./update-call'));
|
||||
api.use('/startConference', require('./start-conference'));
|
||||
|
||||
// health checks
|
||||
api.get('/', (req, res) => res.sendStatus(200));
|
||||
|
||||
41
lib/http-routes/api/start-conference.js
Normal file
41
lib/http-routes/api/start-conference.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('./error');
|
||||
const sessionTracker = require('../../session/session-tracker');
|
||||
const {TaskName} = require('../../utils/constants.json');
|
||||
const {DbErrorUnprocessableRequest} = require('../utils/errors');
|
||||
|
||||
/**
|
||||
* validate the call state
|
||||
*/
|
||||
function retrieveCallSession(callSid, opts) {
|
||||
const cs = sessionTracker.get(callSid);
|
||||
if (cs) {
|
||||
const task = cs.currentTask;
|
||||
if (!task || task.name != TaskName.Conference) {
|
||||
throw new DbErrorUnprocessableRequest(`startConference api failure: indicated call is not waiting: ${task.name}`);
|
||||
}
|
||||
}
|
||||
return cs;
|
||||
}
|
||||
|
||||
/**
|
||||
* update a call
|
||||
*/
|
||||
router.post('/:callSid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const callSid = req.params.callSid;
|
||||
logger.debug({body: req.body}, 'got startConference request');
|
||||
try {
|
||||
const cs = retrieveCallSession(callSid, req.body);
|
||||
if (!cs) {
|
||||
logger.info(`startConference: callSid not found ${callSid}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.status(202).end();
|
||||
cs.notifyStartConference(req.body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -3,7 +3,8 @@ const {CallDirection} = require('./utils/constants');
|
||||
const CallInfo = require('./session/call-info');
|
||||
const Requestor = require('./utils/requestor');
|
||||
const makeTask = require('./tasks/make_task');
|
||||
const normalizeJamones = require('./utils/normalize-jamones');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const normalizeJambones = require('./utils/normalize-jambones');
|
||||
|
||||
module.exports = function(srf, logger) {
|
||||
const {lookupAppByPhoneNumber, lookupAppBySid, lookupAppByRealm} = srf.locals.dbHelpers;
|
||||
@@ -63,7 +64,23 @@ module.exports = function(srf, logger) {
|
||||
|
||||
}
|
||||
}
|
||||
else app = await lookupAppByPhoneNumber(req.locals.calledNumber);
|
||||
else {
|
||||
const uri = parseUri(req.uri);
|
||||
const arr = /context-(.*)/.exec(uri.user);
|
||||
if (arr) {
|
||||
// this is a transfer from another feature server
|
||||
const {retrieveKey, deleteKey} = srf.locals.dbHelpers;
|
||||
try {
|
||||
const obj = JSON.parse(await retrieveKey(arr[1]));
|
||||
logger.info({obj}, 'retrieved application and tasks for a transferred call from realtimedb');
|
||||
app = Object.assign(obj, {transferredCall: true});
|
||||
deleteKey(arr[1]).catch(() => {});
|
||||
} catch (err) {
|
||||
logger.error(err, `Error retrieving transferred call app for ${arr[1]}`);
|
||||
}
|
||||
}
|
||||
else app = await lookupAppByPhoneNumber(req.locals.calledNumber);
|
||||
}
|
||||
|
||||
if (!app || !app.call_hook || !app.call_hook.url) {
|
||||
logger.info(`rejecting call to ${req.locals.calledNumber}: no application or webhook url`);
|
||||
@@ -102,11 +119,17 @@ module.exports = function(srf, logger) {
|
||||
const logger = req.locals.logger;
|
||||
const app = req.locals.application;
|
||||
try {
|
||||
|
||||
if (app.tasks) {
|
||||
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
return next();
|
||||
}
|
||||
/* retrieve the application to execute for this inbound call */
|
||||
const params = Object.assign(app.call_hook.method === 'POST' ? {sip: req.msg} : {},
|
||||
req.locals.callInfo);
|
||||
const json = await app.requestor.request(app.call_hook, params);
|
||||
app.tasks = normalizeJamones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,7 +5,7 @@ const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
const sessionTracker = require('./session-tracker');
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const normalizeJamones = require('../utils/normalize-jamones');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const listTaskNames = require('../utils/summarize-tasks');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
|
||||
@@ -43,7 +43,8 @@ class CallSession extends Emitter {
|
||||
|
||||
this.tmpFiles = new Set();
|
||||
|
||||
sessionTracker.add(this.callSid, this);
|
||||
// if this is a ConfirmSession
|
||||
if (!this.isConfirmCallSession) sessionTracker.add(this.callSid, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +144,27 @@ class CallSession extends Emitter {
|
||||
return this.direction === CallDirection.Inbound && this.res.finalResponseSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the account sid
|
||||
*/
|
||||
get accountSid() {
|
||||
return this.callInfo.accountSid;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if this session was transferred from another server
|
||||
*/
|
||||
get isTransferredCall() {
|
||||
return this.application.transferredCall === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if this session is a ConfirmCallSession
|
||||
*/
|
||||
get isConfirmCallSession() {
|
||||
return this.constructor.name === 'ConfirmCallSession';
|
||||
}
|
||||
|
||||
/**
|
||||
* execute the tasks in the CallSession. The tasks are executed in sequence until
|
||||
* they complete, or the caller hangs up.
|
||||
@@ -178,7 +200,7 @@ class CallSession extends Emitter {
|
||||
this._onTasksDone();
|
||||
this._clearResources();
|
||||
|
||||
sessionTracker.remove(this.callSid);
|
||||
if (!this.isConfirmCallSession) sessionTracker.remove(this.callSid);
|
||||
}
|
||||
|
||||
trackTmpFile(path) {
|
||||
@@ -224,7 +246,7 @@ class CallSession extends Emitter {
|
||||
this.logger.debug('CallSession:_callReleased - caller hung up');
|
||||
this.callGone = true;
|
||||
if (this.currentTask) {
|
||||
this.currentTask.kill();
|
||||
this.currentTask.kill(this);
|
||||
this.currentTask = null;
|
||||
}
|
||||
}
|
||||
@@ -265,7 +287,7 @@ class CallSession extends Emitter {
|
||||
const tasks = await this.requestor.request(opts.call_hook, this.callInfo);
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.logger.info({tasks: listTaskNames(tasks)}, 'CallSession:updateCall new task list');
|
||||
this.replaceApplication(normalizeJamones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
this.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,15 +336,15 @@ class CallSession extends Emitter {
|
||||
if (typeof whisper === 'string' || (typeof whisper === 'object' && whisper.url)) {
|
||||
// retrieve a url
|
||||
const json = await this.requestor(opts.call_hook, this.callInfo);
|
||||
tasks = normalizeJamones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
}
|
||||
else if (Array.isArray(whisper)) {
|
||||
// an inline array of tasks
|
||||
tasks = normalizeJamones(this.logger, whisper).map((tdata) => makeTask(this.logger, tdata));
|
||||
tasks = normalizeJambones(this.logger, whisper).map((tdata) => makeTask(this.logger, tdata));
|
||||
}
|
||||
else if (typeof whisper === 'object') {
|
||||
// a single task
|
||||
tasks = normalizeJamones(this.logger, [whisper]).map((tdata) => makeTask(this.logger, tdata));
|
||||
tasks = normalizeJambones(this.logger, [whisper]).map((tdata) => makeTask(this.logger, tdata));
|
||||
}
|
||||
else {
|
||||
this.logger.info({opts}, 'CallSession:_lccWhisper invalid options were provided');
|
||||
@@ -406,6 +428,18 @@ class CallSession extends Emitter {
|
||||
this.currentTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
kill() {
|
||||
if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)');
|
||||
else this.logger.info('CallSession:kill');
|
||||
if (this.currentTask) {
|
||||
this.currentTask.kill();
|
||||
this.currentTask = null;
|
||||
}
|
||||
this.tasks = [];
|
||||
this.taskIdx = 0;
|
||||
}
|
||||
|
||||
_evaluatePreconditions(task) {
|
||||
switch (task.preconditions) {
|
||||
case TaskPreconditions.None:
|
||||
@@ -426,19 +460,9 @@ class CallSession extends Emitter {
|
||||
* @param {Task} task - task to be executed
|
||||
*/
|
||||
async _evalEndpointPrecondition(task) {
|
||||
this.logger.debug('_evalEndpointPrecondition');
|
||||
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
|
||||
const answerCall = async() => {
|
||||
const uas = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
|
||||
uas.on('destroy', this._callerHungup.bind(this));
|
||||
uas.callSid = this.callSid;
|
||||
uas.connectTime = moment();
|
||||
this.dlg = uas;
|
||||
this.wrapDialog(this.dlg);
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug('CallSession:_evalEndpointPrecondition - answered call');
|
||||
};
|
||||
|
||||
if (this.ep) {
|
||||
if (!task.earlyMedia || this.dlg) return this.ep;
|
||||
|
||||
@@ -454,12 +478,15 @@ class CallSession extends Emitter {
|
||||
ep.cs = this;
|
||||
this.ep = ep;
|
||||
|
||||
this.logger.debug('allocated endpoint');
|
||||
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
if (task.earlyMedia && !this.req.finalResponseSent) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
return ep;
|
||||
}
|
||||
this.propagateAnswer();
|
||||
this.logger.debug('propogating answer');
|
||||
await this.propagateAnswer();
|
||||
}
|
||||
else {
|
||||
// outbound call TODO
|
||||
@@ -495,6 +522,23 @@ class CallSession extends Emitter {
|
||||
return {req: this.req, res: this.res};
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard the current endpoint and allocate a new one, connecting the dialog to it.
|
||||
* This is used, for instance, from the Conference verb when a caller has been
|
||||
* kicked out of conference when a moderator leaves -- the endpoint is destroyed
|
||||
* as well, but the app may want to continue on with other actions
|
||||
*/
|
||||
async replaceEndpoint() {
|
||||
if (!this.dlg) {
|
||||
this.logger.error('CallSession:replaceEndpoint cannot be called without stable dlg');
|
||||
return;
|
||||
}
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
await this.dlg.modify(this.ep.local.sdp);
|
||||
this.logger.debug('CallSession:replaceEndpoint completed');
|
||||
return this.ep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hang up the call and free the media endpoint
|
||||
*/
|
||||
@@ -543,13 +587,14 @@ class CallSession extends Emitter {
|
||||
|
||||
/**
|
||||
* Answer the call, if it has not already been answered.
|
||||
*
|
||||
*
|
||||
* NB: This should be the one and only place we generate 200 OK to incoming INVITEs
|
||||
*/
|
||||
async propagateAnswer() {
|
||||
if (!this.dlg) {
|
||||
assert(this.ep);
|
||||
this.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
|
||||
this.logger.debug('answered call');
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.wrapDialog(this.dlg);
|
||||
this.dlg.callSid = this.callSid;
|
||||
@@ -590,6 +635,45 @@ class CallSession extends Emitter {
|
||||
return {ms: this.ms, ep: this.ep};
|
||||
}
|
||||
|
||||
/**
|
||||
* A conference that the current task is waiting on has just started
|
||||
* @param {*} opts
|
||||
*/
|
||||
notifyStartConference(opts) {
|
||||
if (this.currentTask && typeof this.currentTask.notifyStartConference === 'function') {
|
||||
this.currentTask.notifyStartConference(this, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer the call to another feature server
|
||||
* @param {uri} sip uri to refer the call to
|
||||
*/
|
||||
async referCall(referTo) {
|
||||
assert (this.hasStableDialog);
|
||||
|
||||
const res = await this.dlg.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
'Refer-To': referTo,
|
||||
'Referred-By': `sip:${this.srf.locals.localSipAddress}`
|
||||
}
|
||||
});
|
||||
return [200, 202].includes(res.status);
|
||||
}
|
||||
|
||||
getRemainingTaskData() {
|
||||
const tasks = [...this.tasks];
|
||||
tasks.unshift(this.currentTask);
|
||||
const remainingTasks = [];
|
||||
for (const task of tasks) {
|
||||
const o = {};
|
||||
o[task.name] = task.toJSON();
|
||||
remainingTasks.push(o);
|
||||
}
|
||||
return remainingTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this whenever we answer the A leg, creating a dialog
|
||||
* It wraps the 'destroy' method such that if we hang up the A leg
|
||||
|
||||
578
lib/tasks/conference.js
Normal file
578
lib/tasks/conference.js
Normal file
@@ -0,0 +1,578 @@
|
||||
const Task = require('./task');
|
||||
const Emitter = require('events');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const makeTask = require('./make_task');
|
||||
const bent = require('bent');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const assert = require('assert');
|
||||
const WAIT = 'wait';
|
||||
const JOIN = 'join';
|
||||
const START = 'start';
|
||||
|
||||
function confNoMatch(str) {
|
||||
return str.match(/^No active conferences/) || str.match(/Conference.*not found/);
|
||||
}
|
||||
function getWaitListName(confName) {
|
||||
return `${confName}:waitlist`;
|
||||
}
|
||||
|
||||
function camelize(str) {
|
||||
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(word, index) {
|
||||
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/-/g, '');
|
||||
}
|
||||
|
||||
function unhandled(logger, cs, evt) {
|
||||
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
||||
logger.debug({evt}, `unhandled conference event: ${evt.getHeader('Action')}`) ;
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
if (typeof s !== 'string') return '';
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
class Conference extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.logger = logger;
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
if (!this.data.name) throw new Error('conference name required');
|
||||
|
||||
this.confName = this.data.name;
|
||||
[
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit',
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
|
||||
].forEach((attr) => this[attr] = this.data[attr]);
|
||||
|
||||
this.statusEvents = [];
|
||||
if (this.statusHook) {
|
||||
['start', 'end', 'join', 'leave', 'start-talking', 'stop-talking'].forEach((e) => {
|
||||
if ((this.data.statusEvents || []).includes(e)) this.statusEvents.push(e);
|
||||
});
|
||||
}
|
||||
|
||||
this.emitter = new Emitter();
|
||||
this.results = {};
|
||||
}
|
||||
|
||||
get name() { return TaskName.Conference; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
const dlg = cs.dlg;
|
||||
this.ep.on('destroy', this._kicked.bind(this, cs, dlg));
|
||||
|
||||
try {
|
||||
await this._init(cs, dlg);
|
||||
switch (this.action) {
|
||||
case JOIN:
|
||||
await this._doJoin(cs, dlg);
|
||||
break;
|
||||
case WAIT:
|
||||
await this._doWait(cs, dlg);
|
||||
break;
|
||||
case START:
|
||||
await this._doStart(cs, dlg);
|
||||
break;
|
||||
}
|
||||
await this.awaitTaskDone();
|
||||
|
||||
this.logger.debug(`Conference:exec - conference ${this.confName} is over`);
|
||||
if (this.callMoved !== false) await this.performAction(this.results);
|
||||
} catch (err) {
|
||||
this.logger.info(err, `TaskConference:exec - error in conference ${this.confName}`);
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.info(`Conference:kill ${this.confName}`);
|
||||
this.emitter.emit('kill');
|
||||
await this._doFinalMemberCheck(cs);
|
||||
if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which of three states we are in:
|
||||
* (1) Conference already exists -- we should JOIN
|
||||
* (2) Conference does not exist, and we should START it
|
||||
* (3) Conference does not exist, and we must WAIT for moderator
|
||||
* @param {CallSession} cs
|
||||
* @param {SipDialog} dlg
|
||||
*/
|
||||
async _init(cs, dlg) {
|
||||
const friendlyName = this.confName;
|
||||
const {createHash, retrieveHash} = cs.srf.locals.dbHelpers;
|
||||
this.confName = `conf:${cs.accountSid}:${this.confName}`;
|
||||
|
||||
this.statusParams = Object.assign({
|
||||
conferenceSid: this.confName,
|
||||
friendlyName
|
||||
}, cs.callInfo);
|
||||
|
||||
// check if conference is in progress
|
||||
const obj = await retrieveHash(this.confName);
|
||||
if (obj) {
|
||||
this.logger.info({obj}, `Conference:_init conference ${this.confName} is already started`);
|
||||
this.joinDetails = { conferenceSipAddress: obj.sipAddress};
|
||||
this.conferenceStartTime = new Date(parseInt(obj.startTime));
|
||||
this.statusEvents = obj.statusEvents ? JSON.parse(obj.statusEvents) : [];
|
||||
this.statusHook = obj.statusHook ? JSON.parse(obj.statusHook) : null;
|
||||
this.action = JOIN;
|
||||
}
|
||||
else {
|
||||
if (this.startConferenceOnEnter === false) {
|
||||
this.logger.info(`Conference:_init conference ${this.confName} does not exist, wait for moderator`);
|
||||
this.action = WAIT;
|
||||
}
|
||||
else {
|
||||
this.logger.info(`Conference:_init conference ${this.confName} does not exist, provision it now..`);
|
||||
const obj = {
|
||||
sipAddress: cs.srf.locals.localSipAddress,
|
||||
startTime: Date.now()
|
||||
};
|
||||
if (this.statusEvents.length > 0 && this.statusHook) {
|
||||
Object.assign(obj, {
|
||||
statusEvents: JSON.stringify(this.statusEvents),
|
||||
statusHook: JSON.stringify(this._normalizeHook(cs, this.statusHook))
|
||||
});
|
||||
}
|
||||
const added = await createHash(this.confName, obj);
|
||||
if (added) {
|
||||
this.logger.info(`Conference:_init conference ${this.confName} successfully provisioned`);
|
||||
this.conferenceStartTime = new Date(obj.startTime);
|
||||
this.action = START;
|
||||
}
|
||||
else {
|
||||
this.logger.info(`Conference:_init conference ${this.confName} provision failed..someone beat me to it?`);
|
||||
const obj = await retrieveHash(this.confName);
|
||||
if (null === obj) {
|
||||
this.logger.error(`Conference:_init conference ${this.confName} provision failed again...exiting`);
|
||||
throw new Error('Failed to join conference');
|
||||
}
|
||||
this.joinDetails = { conferenceSipAddress: obj.sipAddress};
|
||||
this.conferenceStartTime = new Date(obj.startTime);
|
||||
this.statusEvents = obj.statusEvents ? JSON.parse(obj.statusEvents) : [];
|
||||
this.statusHook = obj.statusHook ? JSON.parse(obj.statusHook) : null;
|
||||
this.action = JOIN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for entry to a conference, which means
|
||||
* - add ourselves to the waiting list for the conference,
|
||||
* - if provided, continually invoke waitHook to play or say something (pause allowed as well)
|
||||
* - wait for an event indicating the conference has started (or caller hangs up).
|
||||
*
|
||||
* Returns a Promise that is resolved when:
|
||||
* a. caller hangs up while waiting, or
|
||||
* b. conference starts, participant joins the conference
|
||||
* @param {CallSession} cs
|
||||
* @param {SipDialog} dlg
|
||||
*/
|
||||
async _doWait(cs, dlg) {
|
||||
await this._addToWaitList(cs);
|
||||
|
||||
return new Promise(async(resolve, reject) => {
|
||||
this.emitter
|
||||
.once('join', (opts) => {
|
||||
this.joinDetails = opts;
|
||||
this.logger.info({opts}, `time to join conference ${this.confName}`);
|
||||
if (this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
|
||||
// return a Promise that resolves at the end of the conference for this caller
|
||||
this.emitter.removeAllListeners();
|
||||
resolve(this._doJoin(cs, dlg));
|
||||
})
|
||||
.once('kill', () => {
|
||||
this._removeFromWaitList(cs);
|
||||
if (this._playSession) {
|
||||
this.logger.debug('killing waitUrl');
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
if (this.waitHook) {
|
||||
do {
|
||||
try {
|
||||
await this.ep.play('silence_stream://750');
|
||||
const tasks = await this._playHook(cs, dlg, this.waitHook);
|
||||
if (0 === tasks.length) break;
|
||||
} catch (err) {
|
||||
if (!this.joinDetails && !this.killed) {
|
||||
this.logger.info(err, `Conference:_doWait: failed retrieving waitHook for ${this.confName}`);
|
||||
}
|
||||
this._playSession = null;
|
||||
break;
|
||||
}
|
||||
} while (!this.killed && !this.joinDetails);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a conference that has already been started.
|
||||
* The conference may be homed on this feature server, or another one -
|
||||
* in the latter case, move the call to the other server via REFER
|
||||
* @param {CallSession} cs
|
||||
* @param {SipDialog} dlg
|
||||
*/
|
||||
async _doJoin(cs, dlg) {
|
||||
assert(this.joinDetails.conferenceSipAddress);
|
||||
if (cs.srf.locals.localSipAddress !== this.joinDetails.conferenceSipAddress && !cs.isTransferredCall) {
|
||||
this.logger.info({
|
||||
localServer: cs.srf.locals.localSipAddress,
|
||||
confServer: this.joinDetails.conferenceSipAddress
|
||||
}, `Conference:_doJoin: conference ${this.confName} is hosted elsewhere`);
|
||||
const success = await this._doRefer(cs, this.joinDetails.conferenceSipAddress);
|
||||
|
||||
/**
|
||||
* If the REFER succeeded, we will get a BYE from the SBC
|
||||
* which will trigger kill and the end of the execution of the CallSession
|
||||
* which is what we want - so do nothing and let that happen.
|
||||
* If on the other hand, the REFER failed then we are in a bad state
|
||||
* and need to end the conference task with a failure indication and
|
||||
* allow the application to continue on
|
||||
*/
|
||||
if (success) {
|
||||
this.logger.info(`Conference:_doJoin: REFER of ${this.confName} succeeded`);
|
||||
return;
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Conference:_doJoin: conference ${this.confName} is hosted locally`);
|
||||
await this._joinConference(cs, dlg, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a conference and notify anyone on the waiting list
|
||||
* @param {CallSession} cs
|
||||
* @param {SipDialog} dlg
|
||||
*/
|
||||
async _doStart(cs, dlg) {
|
||||
await this._joinConference(cs, dlg, true);
|
||||
|
||||
// notify waiting list members
|
||||
try {
|
||||
const {retrieveSet, deleteKey} = cs.srf.locals.dbHelpers;
|
||||
const setName = getWaitListName(this.confName);
|
||||
const members = await retrieveSet(setName);
|
||||
if (Array.isArray(members) && members.length > 0) {
|
||||
this.logger.info({members}, `Conference:doStart - notifying waiting list for ${this.confName}`);
|
||||
for (const url of members) {
|
||||
try {
|
||||
await bent('POST', 202)(url, {conferenceSipAddress: cs.srf.locals.localSipAddress});
|
||||
} catch (err) {
|
||||
this.logger.info(err, `Failed notifying ${url} to join ${this.confName}`);
|
||||
}
|
||||
}
|
||||
// now clear the waiting list
|
||||
deleteKey(setName);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Conference:_doStart - error notifying wait list');
|
||||
}
|
||||
}
|
||||
|
||||
async _joinConference(cs, dlg, startConf) {
|
||||
if (startConf) {
|
||||
// conference should not exist - check but continue in either case
|
||||
const result = await cs.getMS().api(`conference ${this.confName} list count`);
|
||||
const notFound = typeof result === 'string' && confNoMatch(result);
|
||||
if (!notFound) {
|
||||
this.logger.info({result},
|
||||
`Conference:_joinConference: asked to start ${this.confName} but it unexpectedly exists`);
|
||||
}
|
||||
else {
|
||||
this.participantCount = 0;
|
||||
}
|
||||
this._notifyConferenceEvent(cs, 'start');
|
||||
}
|
||||
|
||||
if (this.enterHook) {
|
||||
try {
|
||||
await this._playHook(cs, dlg, this.enterHook);
|
||||
if (!dlg.connected) {
|
||||
this.logger.debug('Conference:_doJoin: caller hung up during entry prompt');
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, `Error playing enterHook to caller for conference ${this.confName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const opts = {};
|
||||
if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
|
||||
try {
|
||||
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
|
||||
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
|
||||
this.memberId = memberId;
|
||||
this.confUuid = confUuid;
|
||||
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
|
||||
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
|
||||
this._notifyConferenceEvent(cs, 'join');
|
||||
|
||||
// listen for conference events
|
||||
this.ep.filter('Conference-Unique-ID', this.confUuid);
|
||||
this.ep.conn.on('esl::event::CUSTOM::*', this.__onConferenceEvent.bind(this, cs)) ;
|
||||
|
||||
// optionally play beep to conference on entry
|
||||
if (this.beep === true) {
|
||||
this.ep.api('conference',
|
||||
[this.confName, 'play', 'tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)'])
|
||||
.catch((err) => {});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.logger.error(err, `Failed to join conference ${this.confName}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (typeof this.maxParticipants === 'number' && this.maxParticipants > 1) {
|
||||
this.endpoint.api('conference', `${this.confName} set max_members ${this.maxParticipants}`)
|
||||
.catch((err) => this.logger.error(err, `Error setting max participants to ${this.maxParticipants}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The conference we have been waiting for has started.
|
||||
* It may be on this server or a different one, and we are
|
||||
* given instructions how to find it and connect.
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.confName name of the conference
|
||||
* @param {string} opts.conferenceSipAddress ip:port of the feature server hosting the conference
|
||||
*/
|
||||
notifyStartConference(cs, opts) {
|
||||
this.logger.info({opts}, `Conference:notifyStartConference: conference ${this.confName} has now started`);
|
||||
this.emitter.emit('join', opts);
|
||||
}
|
||||
|
||||
async _doRefer(cs, sipAddress) {
|
||||
const uuid = uuidv4();
|
||||
const {addKey} = cs.srf.locals.dbHelpers;
|
||||
const obj = Object.assign({}, cs.application);
|
||||
delete obj.requestor;
|
||||
delete obj.notifier;
|
||||
obj.tasks = cs.getRemainingTaskData();
|
||||
|
||||
this.logger.debug({obj}, 'Conference:_doRefer');
|
||||
|
||||
const success = await addKey(uuid, JSON.stringify(obj), 30);
|
||||
if (!success) {
|
||||
this.logger.info(`Conference:_doRefer failed storing task data before REFER for ${this.confName}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.logger.info(`Conference:_doRefer: referring call to ${sipAddress} for ${this.confName}`);
|
||||
this.callMoved = true;
|
||||
const success = await cs.referCall(`sip:context-${uuid}@${sipAddress}`);
|
||||
if (!success) {
|
||||
this.callMoved = false;
|
||||
this.logger.info('Conference:_doRefer REFER failed');
|
||||
return success;
|
||||
}
|
||||
this.logger.info('Conference:_doRefer REFER succeeded');
|
||||
return success;
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Conference:_doRefer error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ourselves to the waitlist of sessions to be notified once
|
||||
* the conference starts
|
||||
* @param {CallSession} cs
|
||||
*/
|
||||
async _addToWaitList(cs) {
|
||||
const {addToSet} = cs.srf.locals.dbHelpers;
|
||||
const setName = getWaitListName(this.confName);
|
||||
const url = `${cs.srf.locals.serviceUrl}/v1/startConference/${cs.callSid}`;
|
||||
const added = await addToSet(setName, url);
|
||||
if (added !== 1) throw new Error(`failed adding to the waitlist for conference ${this.confName}: ${added}`);
|
||||
this.logger.debug(`successfully added to the waiting list for conference ${this.confName}`);
|
||||
}
|
||||
|
||||
async _removeFromWaitList(cs) {
|
||||
const {removeFromSet} = cs.srf.locals.dbHelpers;
|
||||
const setName = getWaitListName(this.confName);
|
||||
const url = `${cs.srf.locals.serviceUrl}/v1/startConference/${cs.callSid}`;
|
||||
try {
|
||||
const count = await removeFromSet(setName, url);
|
||||
this.logger.debug(`Conference:_removeFromWaitList removed ${count} from waiting list`);
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'Error removing from waiting list');
|
||||
}
|
||||
}
|
||||
|
||||
_normalizeHook(cs, hook) {
|
||||
if (typeof hook === 'object') return hook;
|
||||
const url = hook.startsWith('/') ?
|
||||
`${cs.application.requestor.baseUrl}${hook}` :
|
||||
hook;
|
||||
|
||||
return { url } ;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are the last one leaving the conference - turn out the lights.
|
||||
* Remove the conference info from the realtime database.
|
||||
* @param {*} cs
|
||||
*/
|
||||
async _doFinalMemberCheck(cs) {
|
||||
if (!this.memberId) return; // never actually joined
|
||||
|
||||
this.logger.debug(`Conference:_doFinalMemberCheck leaving ${this.confName} member count: ${this.participantCount}`);
|
||||
try {
|
||||
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
|
||||
if (response.body && confNoMatch(response.body)) this.participantCount = 0;
|
||||
else if (response.body && /^\d+$/.test(response.body)) this.participantCount = parseInt(response.body) - 1;
|
||||
this.logger.debug({response}, `Conference:_doFinalMemberCheck conference count ${this.participantCount}`);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked');
|
||||
}
|
||||
await this._notifyConferenceEvent(cs, 'leave');
|
||||
|
||||
/**
|
||||
* when we hang up as the last member, the current member count = 1
|
||||
* when we are kicked out of the call when the moderator leaves, the member count = 0
|
||||
*/
|
||||
if (this.participantCount === 0) {
|
||||
const {deleteKey} = cs.srf.locals.dbHelpers;
|
||||
try {
|
||||
this._notifyConferenceEvent(cs, 'end');
|
||||
const removed = await deleteKey(this.confName);
|
||||
this.logger.info(`conf ${this.confName} deprovisioned: ${removed ? 'success' : 'failure'}`);
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.error(err, `Error deprovisioning conference ${this.confName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 allowedTasks = json.filter((task) => allowed.includes(task.verb));
|
||||
if (json.length !== allowedTasks.length) {
|
||||
this.logger.debug({json, allowedTasks}, 'unsupported task');
|
||||
throw new Error(`unsupported verb in dial conference wait/enterHook: only ${JSON.stringify(allowed)}`);
|
||||
}
|
||||
this.logger.debug(`Conference:_playHook: executing ${json.length} tasks`);
|
||||
|
||||
if (json.length > 0) {
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
this._playSession = new ConfirmCallSession({
|
||||
logger: this.logger,
|
||||
application: cs.application,
|
||||
dlg,
|
||||
ep: cs.ep,
|
||||
callInfo: cs.callInfo,
|
||||
tasks
|
||||
});
|
||||
await this._playSession.exec();
|
||||
this._playSession = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* This event triggered when we are bounced from conference when moderator leaves.
|
||||
* Get a new endpoint up and running in case the app wants to go on (e.g post-call survey)
|
||||
* @param {*} cs CallSession
|
||||
* @param {*} dlg SipDialog
|
||||
*/
|
||||
_kicked(cs, dlg) {
|
||||
this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`);
|
||||
this.replaceEndpointAndEnd(cs);
|
||||
}
|
||||
|
||||
async replaceEndpointAndEnd(cs) {
|
||||
if (this.replaced) return;
|
||||
this.replaced = true;
|
||||
try {
|
||||
this.ep = await cs.replaceEndpoint();
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Conference:replaceEndpointAndEnd failed');
|
||||
}
|
||||
this.kill(cs);
|
||||
}
|
||||
|
||||
_notifyConferenceEvent(cs, eventName, params = {}) {
|
||||
if (this.statusEvents.includes(eventName)) {
|
||||
params.event = eventName;
|
||||
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))
|
||||
.catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error'));
|
||||
}
|
||||
}
|
||||
|
||||
__onConferenceEvent(cs, evt) {
|
||||
const eventName = evt.getHeader('Event-Subclass') ;
|
||||
if (eventName === 'conference::maintenance') {
|
||||
const action = evt.getHeader('Action') ;
|
||||
|
||||
//invoke a handler for this action, if we have defined one
|
||||
const functionName = `_on${capitalize(camelize(action))}`;
|
||||
(Conference.prototype[functionName] || unhandled).bind(this, this.logger, cs, evt)() ;
|
||||
}
|
||||
else {
|
||||
this.logger.debug(`Conference#__onConferenceEvent: got unhandled custom event: ${eventName}`) ;
|
||||
}
|
||||
}
|
||||
|
||||
// conference event handlers
|
||||
_onDelMember(logger, cs, evt) {
|
||||
const memberId = parseInt(evt.getHeader('Member-ID')) ;
|
||||
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
||||
if (memberId === this.memberId) {
|
||||
this.logger.info(`Conference:_onDelMember - I was dropped from conference ${this.confName}, task is complete`);
|
||||
this.replaceEndpointAndEnd(cs);
|
||||
}
|
||||
}
|
||||
|
||||
_onStartTalking(logger, cs, evt) {
|
||||
const memberId = parseInt(evt.getHeader('Member-ID')) ;
|
||||
const size = this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
||||
if (memberId === this.memberId) {
|
||||
const time = new Date(evt.getHeader('Event-Date-Timestamp') / 1000).toISOString();
|
||||
this._notifyConferenceEvent(cs, 'start-talking', {
|
||||
time,
|
||||
members: size
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onStopTalking(logger, cs, evt) {
|
||||
const memberId = parseInt(evt.getHeader('Member-ID')) ;
|
||||
const size = this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
||||
if (memberId === this.memberId) {
|
||||
const time = new Date(evt.getHeader('Event-Date-Timestamp') / 1000).toISOString();
|
||||
this._notifyConferenceEvent(cs, 'stop-talking', {
|
||||
time,
|
||||
members: size
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Conference;
|
||||
@@ -116,7 +116,7 @@ class TaskDial extends Task {
|
||||
get name() { return TaskName.Dial; }
|
||||
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
try {
|
||||
if (cs.direction === CallDirection.Inbound) {
|
||||
await this._initializeInbound(cs);
|
||||
@@ -135,12 +135,12 @@ class TaskDial extends Task {
|
||||
this._removeDtmfDetection(cs, this.ep);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskDial:exec terminating with error');
|
||||
this.kill();
|
||||
this.kill(cs);
|
||||
}
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this._removeDtmfDetection(this.cs, this.epOther);
|
||||
this._removeDtmfDetection(this.cs, this.ep);
|
||||
this._killOutdials();
|
||||
@@ -149,8 +149,8 @@ class TaskDial extends Task {
|
||||
this.sd = null;
|
||||
}
|
||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||
if (this.listenTask) await this.listenTask.kill();
|
||||
if (this.transcribeTask) await this.transcribeTask.kill();
|
||||
if (this.listenTask) await this.listenTask.kill(cs);
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
@@ -279,7 +279,7 @@ class TaskDial extends Task {
|
||||
this.dials.delete(sd.callSid);
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after call create err, ending task');
|
||||
this.kill();
|
||||
this.kill(cs);
|
||||
}
|
||||
})
|
||||
.on('callStatusChange', (obj) => {
|
||||
@@ -308,7 +308,7 @@ class TaskDial extends Task {
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after call failure, ending task');
|
||||
clearTimeout(timerRing);
|
||||
this.kill();
|
||||
this.kill(cs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -322,7 +322,7 @@ class TaskDial extends Task {
|
||||
this.dials.delete(sd.callSid);
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending task');
|
||||
this.kill();
|
||||
this.kill(cs);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -367,7 +367,7 @@ class TaskDial extends Task {
|
||||
this.timerMaxCallDuration = setTimeout(() => {
|
||||
this.logger.info(`Dial:_selectSingleDial tearing down call as it has reached ${this.timeLimit}s`);
|
||||
this.ep.unbridge();
|
||||
this.kill();
|
||||
this.kill(cs);
|
||||
}, this.timeLimit * 1000);
|
||||
}
|
||||
sessionTracker.add(this.callSid, cs);
|
||||
@@ -376,7 +376,7 @@ class TaskDial extends Task {
|
||||
sessionTracker.remove(this.callSid);
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.ep.unbridge();
|
||||
this.kill();
|
||||
this.kill(cs);
|
||||
});
|
||||
|
||||
Object.assign(this.results, {
|
||||
|
||||
@@ -36,7 +36,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
|
||||
try {
|
||||
@@ -71,8 +71,8 @@ class TaskGather extends Task {
|
||||
ep.removeCustomEventListener(TranscriptionEvents.EndOfUtterance);
|
||||
}
|
||||
|
||||
kill() {
|
||||
super.kill();
|
||||
kill(cs) {
|
||||
super.kill(cs);
|
||||
this._killAudio();
|
||||
this._resolve('killed');
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class TaskHangup extends Task {
|
||||
* Hangup the call
|
||||
*/
|
||||
async exec(cs, dlg) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
try {
|
||||
await dlg.destroy({headers: this.headers});
|
||||
} catch (err) {
|
||||
|
||||
@@ -29,7 +29,7 @@ class TaskListen extends Task {
|
||||
get name() { return TaskName.Listen; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
|
||||
try {
|
||||
@@ -50,8 +50,8 @@ class TaskListen extends Task {
|
||||
this._removeListeners(ep);
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`);
|
||||
this._clearTimer();
|
||||
if (this.ep && this.ep.connected) {
|
||||
@@ -63,7 +63,7 @@ class TaskListen extends Task {
|
||||
const duration = moment().diff(this.recordStartTime, 'seconds');
|
||||
this.results.dialCallDuration = duration;
|
||||
}
|
||||
if (this.transcribeTask) await this.transcribeTask.kill();
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ class TaskListen extends Task {
|
||||
if (this.maxLength) {
|
||||
this._timer = setTimeout(() => {
|
||||
this.logger.debug(`TaskListen terminating task due to timeout of ${this.timeout}s reached`);
|
||||
this.kill();
|
||||
this.kill(cs);
|
||||
}, this.maxLength * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ function makeTask(logger, obj, parent) {
|
||||
}
|
||||
const name = keys[0];
|
||||
const data = obj[name];
|
||||
//logger.debug(data, `makeTask: ${name}`);
|
||||
if (typeof data !== 'object') {
|
||||
throw errBadInstruction;
|
||||
}
|
||||
@@ -18,6 +17,10 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.SipDecline:
|
||||
const TaskSipDecline = require('./sip_decline');
|
||||
return new TaskSipDecline(logger, data, parent);
|
||||
case TaskName.Conference:
|
||||
logger.debug({data}, 'Conference verb');
|
||||
const TaskConference = require('./conference');
|
||||
return new TaskConference(logger, data, parent);
|
||||
case TaskName.Dial:
|
||||
const TaskDial = require('./dial');
|
||||
return new TaskDial(logger, data, parent);
|
||||
@@ -25,11 +28,6 @@ function makeTask(logger, obj, parent) {
|
||||
const TaskHangup = require('./hangup');
|
||||
return new TaskHangup(logger, data, parent);
|
||||
case TaskName.Say:
|
||||
if (data.synthesizer.vendor === 'google' && !data.synthesizer.language) {
|
||||
logger.debug('creating legacy say task');
|
||||
const TaskSayLegacy = require('./say-legacy');
|
||||
return new TaskSayLegacy(logger, data, parent);
|
||||
}
|
||||
const TaskSay = require('./say');
|
||||
return new TaskSay(logger, data, parent);
|
||||
case TaskName.Play:
|
||||
|
||||
@@ -11,13 +11,13 @@ class TaskPause extends Task {
|
||||
get name() { return TaskName.Pause; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
this.timer = setTimeout(this.notifyTaskDone.bind(this), this.length * 1000);
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
clearTimeout(this.timer);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class TaskPlay extends Task {
|
||||
get name() { return TaskName.Play; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
try {
|
||||
while (!this.killed && this.loop--) {
|
||||
@@ -26,8 +26,8 @@ class TaskPlay extends Task {
|
||||
this.emit('playDone');
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected && !this.playComplete) {
|
||||
this.logger.debug('TaskPlay:kill - killing audio');
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
|
||||
@@ -12,7 +12,7 @@ class TaskRedirect extends Task {
|
||||
get name() { return TaskName.Redirect; }
|
||||
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
await this.performAction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const normalizeJamones = require('../utils/normalize-jamones');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
|
||||
/**
|
||||
* Manages an outdial made via REST API
|
||||
@@ -25,15 +25,15 @@ class TaskRestDial extends Task {
|
||||
* INVITE has just been sent at this point
|
||||
*/
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
this.req = cs.req;
|
||||
|
||||
this._setCallTimer();
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
|
||||
kill() {
|
||||
super.kill();
|
||||
kill(cs) {
|
||||
super.kill(cs);
|
||||
this._clearCallTimer();
|
||||
if (this.req) {
|
||||
this.req.cancel();
|
||||
@@ -51,7 +51,7 @@ class TaskRestDial extends Task {
|
||||
const tasks = await cs.requestor.request(this.call_hook, cs.callInfo);
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
||||
cs.replaceApplication(normalizeJamones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'TaskRestDial:_onConnect error retrieving or parsing application, ending call');
|
||||
|
||||
@@ -17,7 +17,7 @@ class TaskSay extends Task {
|
||||
async exec(cs, ep) {
|
||||
const {srf} = cs;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
super.exec(cs);
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
try {
|
||||
let filepath;
|
||||
@@ -42,8 +42,8 @@ class TaskSay extends Task {
|
||||
this.emit('playDone');
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected) {
|
||||
this.logger.debug('TaskSay:kill - killing audio');
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
|
||||
@@ -54,6 +54,23 @@
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"conference": {
|
||||
"properties": {
|
||||
"name": "string",
|
||||
"beep": "boolean",
|
||||
"startConferenceOnEnter": "boolean",
|
||||
"endConferenceOnExit": "boolean",
|
||||
"maxParticipants": "number",
|
||||
"actionHook": "object|string",
|
||||
"waitHook": "object|string",
|
||||
"statusEvents": "array",
|
||||
"statusHook": "object|string",
|
||||
"enterHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"dial": {
|
||||
"properties": {
|
||||
"actionHook": "object|string",
|
||||
@@ -165,10 +182,10 @@
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"name": "string",
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
"auth": "#auth",
|
||||
"name": "string"
|
||||
"auth": "#auth"
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
|
||||
@@ -2,7 +2,7 @@ const Emitter = require('events');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const assert = require('assert');
|
||||
const {TaskPreconditions} = require('../utils/constants');
|
||||
const normalizeJamones = require('../utils/normalize-jamones');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const specs = new Map();
|
||||
const _specData = require('./specs');
|
||||
for (const key in _specData) {specs.set(key, _specData[key]);}
|
||||
@@ -55,8 +55,8 @@ class Task extends Emitter {
|
||||
* called to kill (/stop) a running task
|
||||
* what to do is up to each type of task
|
||||
*/
|
||||
kill() {
|
||||
this.logger.debug(`${this.name} is being killed`);
|
||||
kill(cs) {
|
||||
if (this.cs && !this.cs.isConfirmCallSession) this.logger.debug(`${this.name} is being killed`);
|
||||
this._killInProgress = true;
|
||||
// no-op
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class Task extends Emitter {
|
||||
const json = await this.cs.requestor.request(this.actionHook, params);
|
||||
if (expectResponse && json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJamones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.callSession.replaceApplication(tasks);
|
||||
|
||||
@@ -32,8 +32,8 @@ class TaskTranscribe extends Task {
|
||||
ep.removeCustomEventListener(TranscriptionEvents.MaxDurationExceeded);
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected) {
|
||||
this.ep.stopTranscription().catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"TaskName": {
|
||||
"Conference": "conference",
|
||||
"Dial": "dial",
|
||||
"Gather": "gather",
|
||||
"Hangup": "hangup",
|
||||
|
||||
@@ -114,7 +114,15 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveCall,
|
||||
listCalls,
|
||||
deleteCall,
|
||||
synthAudio
|
||||
synthAudio,
|
||||
createHash,
|
||||
retrieveHash,
|
||||
deleteKey,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
retrieveSet,
|
||||
addToSet,
|
||||
removeFromSet
|
||||
} = require('jambonz-realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
@@ -129,7 +137,15 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveCall,
|
||||
listCalls,
|
||||
deleteCall,
|
||||
synthAudio
|
||||
synthAudio,
|
||||
createHash,
|
||||
retrieveHash,
|
||||
deleteKey,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
retrieveSet,
|
||||
addToSet,
|
||||
removeFromSet
|
||||
},
|
||||
parentLogger: logger,
|
||||
ipv4: localIp,
|
||||
|
||||
@@ -77,7 +77,7 @@ class Requestor {
|
||||
try {
|
||||
buf = isRelativeUrl(url) ?
|
||||
await this.post(url, params, this.authHeader) :
|
||||
await bent(method, 'buffer', 200, 201)(url, params, basicAuth(username, password));
|
||||
await bent(method, 'buffer', 200, 201, 202)(url, params, basicAuth(username, password));
|
||||
} catch (err) {
|
||||
this.logger.info({baseUrl: this.baseUrl, url: err.statusCode},
|
||||
`web callback returned unexpected error code ${err.statusCode}`);
|
||||
@@ -95,7 +95,7 @@ class Requestor {
|
||||
return json;
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.debug({err, url, method}, `Requestor:request returned non-JSON content: '${buf.toString()}'`);
|
||||
//this.logger.debug({err, url, method}, `Requestor:request returned non-JSON content: '${buf.toString()}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ module.exports = (logger) => {
|
||||
uri: `sip:${sbc}`,
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed'
|
||||
'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed',
|
||||
'X-FS-Calls': srf.locals.sessionTracker.count
|
||||
}
|
||||
});
|
||||
req.on('response', (res) => {
|
||||
@@ -91,7 +92,13 @@ module.exports = (logger) => {
|
||||
setInterval(() => {
|
||||
const {srf} = require('../..');
|
||||
pingProxies(srf);
|
||||
}, 60000);
|
||||
}, 20000);
|
||||
|
||||
// initial ping once we are up
|
||||
setTimeout(() => {
|
||||
const {srf} = require('../..');
|
||||
pingProxies(srf);
|
||||
}, 1000);
|
||||
|
||||
return {
|
||||
lifecycleEmitter,
|
||||
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -252,25 +252,25 @@
|
||||
}
|
||||
},
|
||||
"@google-cloud/text-to-speech": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/text-to-speech/-/text-to-speech-2.2.0.tgz",
|
||||
"integrity": "sha512-RM0CQEHs7HdtS0nYNHrK5a0zgoqS47OxTdQQJ7jdm3+JuEfZyIM2Xw+VOJvZpAClv/Ac64DQwLeA0y4TYZ3sRw==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/text-to-speech/-/text-to-speech-2.3.0.tgz",
|
||||
"integrity": "sha512-Rjx/lN7FjA6YYXcOmC707d1gKxkGS5F6nxs+R6QcssJwZbIfZ9/nhyl0m3jZU39dNe+yT9CeJynp3jaZ9G7Bzw==",
|
||||
"requires": {
|
||||
"google-gax": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@grpc/grpc-js": {
|
||||
"version": "0.6.18",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.18.tgz",
|
||||
"integrity": "sha512-uAzv/tM8qpbf1vpx1xPMfcUMzbfdqJtdCYAqY/LsLeQQlnTb4vApylojr+wlCyr7bZeg3AFfHvtihnNOQQt/nA==",
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.7.9.tgz",
|
||||
"integrity": "sha512-ihn9xWOqubMPBlU77wcYpy7FFamGo5xtsK27EAILL/eoOvGEAq29UOrqRvqYPwWfl2+3laFmGKNR7uCdJhKu4Q==",
|
||||
"requires": {
|
||||
"semver": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"@grpc/proto-loader": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.3.tgz",
|
||||
"integrity": "sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ==",
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz",
|
||||
"integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==",
|
||||
"requires": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"protobufjs": "^6.8.6"
|
||||
@@ -368,9 +368,9 @@
|
||||
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.17.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.17.tgz",
|
||||
"integrity": "sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q=="
|
||||
"version": "13.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz",
|
||||
"integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A=="
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
@@ -519,9 +519,9 @@
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.637.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.637.0.tgz",
|
||||
"integrity": "sha512-e7EYX5rNtQyEaleQylUtLSNKXOmvOwfifQ4bYkfF80mFsVI3DSydczLHXrqPzXoEJaS/GI/9HqVnlQcPs6Q3ew==",
|
||||
"version": "2.663.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.663.0.tgz",
|
||||
"integrity": "sha512-xPOszNOaSXTRs8VGXaMbhTKXdlq2TlDRfFRVEGxkZrtow87hEIVZGAUSUme2e3GHqHUDnySwcufrUpUPUizOKQ==",
|
||||
"requires": {
|
||||
"buffer": "4.9.1",
|
||||
"events": "1.1.1",
|
||||
@@ -1016,15 +1016,10 @@
|
||||
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
|
||||
"integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
|
||||
},
|
||||
"drachtio-fn-b2b-sugar": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fn-b2b-sugar/-/drachtio-fn-b2b-sugar-0.0.12.tgz",
|
||||
"integrity": "sha512-FKPAcEMJTYKDrd9DJUCc4VHnY/c65HOO9k8XqVNognF9T02hKEjGuBCM4Da9ipyfiHmVRuECwj0XNvZ361mkVQ=="
|
||||
},
|
||||
"drachtio-fsmrf": {
|
||||
"version": "1.5.14",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-1.5.14.tgz",
|
||||
"integrity": "sha512-QgHkI1FT35ECLiQBqx28yc1QL5g1hn7cAzXm9ej5J+dfjuhxAdiN9D2dPmYZ1sqBOB9RWZH+ajBeZiT4zM9QnQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-2.0.1.tgz",
|
||||
"integrity": "sha512-XVO4OSgBoxZMkIer5RZXv38A52UXfLLTd9P45NGSQA+TCBYBxP/8NRcOu+Sp/+wYVPDQtFVDuhxWvggHF9nHPQ==",
|
||||
"requires": {
|
||||
"async": "^1.4.2",
|
||||
"debug": "^2.2.0",
|
||||
@@ -1474,9 +1469,9 @@
|
||||
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
|
||||
},
|
||||
"fast-text-encoding": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.1.tgz",
|
||||
"integrity": "sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ=="
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.2.tgz",
|
||||
"integrity": "sha512-5rQdinSsycpzvAoHga2EDn+LRX1d5xLFsuNG0Kg61JrAT/tASXcLL0nf/33v+sAxlQcfYmWbTURa1mmAf55jGw=="
|
||||
},
|
||||
"figures": {
|
||||
"version": "3.2.0",
|
||||
@@ -1681,22 +1676,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"gaxios": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.2.tgz",
|
||||
"integrity": "sha512-K/+py7UvKRDaEwEKlLiRKrFr+wjGjsMz5qH7Vs549QJS7cpSCOT/BbWL7pzqECflc46FcNPipjSfB+V1m8PAhw==",
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz",
|
||||
"integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==",
|
||||
"requires": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"gcp-metadata": {
|
||||
@@ -1792,11 +1780,11 @@
|
||||
}
|
||||
},
|
||||
"google-gax": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.14.2.tgz",
|
||||
"integrity": "sha512-Nde+FdqALbV3QgMA4KlkxOHfrj9busnZ3EECwy/1gDJm9vhKGwDLWzErqRU5g80OoGSAMgyY7DWIfqz7ina4Jw==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.15.2.tgz",
|
||||
"integrity": "sha512-yNNiRf9QxWpZNfQQmSPz3rIDTBDDKnLKY/QEsjCaJyDxttespr6v8WRGgU5KrU/6ZM7QRlgBAYXCkxqHhJp0wA==",
|
||||
"requires": {
|
||||
"@grpc/grpc-js": "^0.6.18",
|
||||
"@grpc/grpc-js": "^0.7.4",
|
||||
"@grpc/proto-loader": "^0.5.1",
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
"@types/long": "^4.0.0",
|
||||
@@ -1807,7 +1795,7 @@
|
||||
"lodash.at": "^4.6.0",
|
||||
"lodash.has": "^4.5.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"protobufjs": "^6.8.8",
|
||||
"protobufjs": "^6.8.9",
|
||||
"retry-request": "^4.0.0",
|
||||
"semver": "^6.0.0",
|
||||
"walkdir": "^0.4.0"
|
||||
@@ -2396,9 +2384,9 @@
|
||||
}
|
||||
},
|
||||
"jambonz-realtimedb-helpers": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jambonz-realtimedb-helpers/-/jambonz-realtimedb-helpers-0.2.2.tgz",
|
||||
"integrity": "sha512-aOBQBB/RVilXXCXWZRyJcgAZh1mYk9YsinOVdPXax+QKDVoKGY43mngYQ/7SRfCagcsHrc+cpzwGSox7Aixiew==",
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/jambonz-realtimedb-helpers/-/jambonz-realtimedb-helpers-0.2.11.tgz",
|
||||
"integrity": "sha512-/QWjRgCvBcZyzt4NfKKDA54Cm4/tEzXH4QkLN4Yy/ZjGZYHaY2QRd76A6rV8/gnTlgoVDUd2iH0zyapLGZg7Aw==",
|
||||
"requires": {
|
||||
"@google-cloud/text-to-speech": "^2.2.0",
|
||||
"aws-sdk": "^2.631.0",
|
||||
@@ -3083,9 +3071,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"protobufjs": {
|
||||
"version": "6.8.8",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
|
||||
"integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==",
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.9.0.tgz",
|
||||
"integrity": "sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg==",
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
@@ -3097,8 +3085,8 @@
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/long": "^4.0.0",
|
||||
"@types/node": "^10.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^13.7.0",
|
||||
"long": "^4.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,14 +29,13 @@
|
||||
"bent": "^7.0.6",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.1.1",
|
||||
"drachtio-fn-b2b-sugar": "0.0.12",
|
||||
"drachtio-fsmrf": "^1.5.14",
|
||||
"drachtio-fsmrf": "^2.0.1",
|
||||
"drachtio-srf": "^4.4.28",
|
||||
"express": "^4.17.1",
|
||||
"ip": "^1.1.5",
|
||||
"jambonz-db-helpers": "^0.3.2",
|
||||
"jambonz-mw-registrar": "^0.1.3",
|
||||
"jambonz-realtimedb-helpers": "^0.2.2",
|
||||
"jambonz-realtimedb-helpers": "^0.2.11",
|
||||
"jambonz-stats-collector": "^0.0.3",
|
||||
"moment": "^2.24.0",
|
||||
"parse-url": "^5.0.1",
|
||||
|
||||
@@ -40,7 +40,7 @@ test('app payload parsing tests', (t) => {
|
||||
t.ok(task.name === 'pause', 'parsed pause');
|
||||
|
||||
const alt = require('./data/good/alternate-syntax');
|
||||
const normalize = require('../lib/utils/normalize-jamones');
|
||||
const normalize = require('../lib/utils/normalize-jambones');
|
||||
normalize(logger, alt).forEach((t) => {
|
||||
const task = makeTask(logger, t);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user