mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-19 04:17:44 +00:00
major revamp of http client functionalit
This commit is contained in:
1
app.js
1
app.js
@@ -59,7 +59,6 @@ srf.invite((req, res) => {
|
||||
session.exec();
|
||||
});
|
||||
|
||||
|
||||
// HTTP
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
const config = require('config');
|
||||
const router = require('express').Router();
|
||||
const sysError = require('./error');
|
||||
const makeTask = require('../../tasks/make_task');
|
||||
const RestCallSession = require('../../session/rest-call-session');
|
||||
const CallInfo = require('../../session/call-info');
|
||||
const {CallDirection, CallStatus} = require('../../utils/constants');
|
||||
const parseUrl = require('parse-url');
|
||||
const SipError = require('drachtio-srf').SipError;
|
||||
const Srf = require('drachtio-srf');
|
||||
const sysError = require('./error');
|
||||
const drachtio = config.get('outdials.drachtio');
|
||||
const sbcs = config.get('outdials.sbc');
|
||||
const Mrf = require('drachtio-fsmrf');
|
||||
const installSrfLocals = require('../../utils/install-srf-locals');
|
||||
const Requestor = require('./utils/requestor');
|
||||
|
||||
let idxDrachtio = 0;
|
||||
let idxSbc = 0;
|
||||
let srfs = [];
|
||||
@@ -57,41 +58,16 @@ function getSrfForOutdial(logger) {
|
||||
});
|
||||
}
|
||||
|
||||
async function validate(logger, payload) {
|
||||
const data = Object.assign({}, {
|
||||
from: payload.from,
|
||||
to: payload.to,
|
||||
call_hook: payload.call_hook
|
||||
});
|
||||
|
||||
const u = parseUrl(payload.call_hook.url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
payload.originalRequest = {
|
||||
baseUrl: `${u.protocol}://${u.resource}${myPort}`,
|
||||
method: payload.call_hook.method
|
||||
};
|
||||
if (payload.call_hook.username && payload.call_hook.password) {
|
||||
payload.originalRequest.auth = {
|
||||
username: payload.call_hook.username,
|
||||
password: payload.call_hook.password
|
||||
};
|
||||
}
|
||||
|
||||
return makeTask(logger, {'rest:dial': data});
|
||||
}
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
logger.debug({body: req.body}, 'got createCall request');
|
||||
try {
|
||||
let uri, cs, to;
|
||||
const restDial = await validate(logger, req.body);
|
||||
const restDial = makeTask(logger, {'rest:dial': req.body});
|
||||
const sbcAddress = sbcs[idxSbc++ % sbcs.length];
|
||||
const srf = await getSrfForOutdial(logger);
|
||||
const target = restDial.to;
|
||||
const opts = {
|
||||
'callingNumber': restDial.from
|
||||
};
|
||||
const opts = { callingNumber: restDial.from };
|
||||
|
||||
switch (target.type) {
|
||||
case 'phone':
|
||||
@@ -130,8 +106,24 @@ router.post('/', async(req, res) => {
|
||||
localSdp: ep.local.sdp
|
||||
});
|
||||
if (target.auth) opts.auth = this.target.auth;
|
||||
const application = req.body;
|
||||
|
||||
|
||||
/**
|
||||
* create our application object -
|
||||
* not from the database as per an inbound call,
|
||||
* but from the provided params in the request
|
||||
*/
|
||||
const app = req.body;
|
||||
|
||||
/**
|
||||
* attach our requestor and notifier objects
|
||||
* these will be used for all http requests we make during this call
|
||||
*/
|
||||
app.requestor = new Requestor(this.logger, app.call_hook);
|
||||
if (app.call_status_hook) app.notifier = new Requestor(this.logger, app.call_status_hook);
|
||||
else app.notifier = {request: () => {}};
|
||||
|
||||
/* now launch the outdial */
|
||||
try {
|
||||
const dlg = await srf.createUAC(uri, opts, {
|
||||
cbRequest: (err, inviteReq) => {
|
||||
@@ -140,18 +132,18 @@ router.post('/', async(req, res) => {
|
||||
res.status(500).send('Call Failure');
|
||||
ep.destroy();
|
||||
}
|
||||
/* ok our outbound NVITE is in flight */
|
||||
|
||||
/* call is in flight */
|
||||
const tasks = [restDial];
|
||||
const callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
req: inviteReq,
|
||||
to,
|
||||
tag: req.body.tag,
|
||||
tag: app.tag,
|
||||
accountSid: req.body.account_sid,
|
||||
applicationSid: req.body.application_sid
|
||||
applicationSid: app.application_sid
|
||||
});
|
||||
cs = new RestCallSession({logger, application, srf, req: inviteReq, ep, tasks, callInfo});
|
||||
cs = new RestCallSession({logger, application: app, srf, req: inviteReq, ep, tasks, callInfo});
|
||||
cs.exec(req);
|
||||
|
||||
res.status(201).json({sid: cs.callSid});
|
||||
@@ -191,7 +183,6 @@ router.post('/', async(req, res) => {
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -31,6 +31,10 @@ function retrieveCallSession(callSid, opts) {
|
||||
|
||||
return cs;
|
||||
}
|
||||
|
||||
/**
|
||||
* update a call
|
||||
*/
|
||||
router.post('/:callSid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const callSid = req.params.callSid;
|
||||
@@ -42,7 +46,7 @@ router.post('/:callSid', async(req, res) => {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.sendStatus(202);
|
||||
cs.updateCall(req.body);
|
||||
cs.updateCall(req.body, callSid);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//const debug = require('debug')('jambonz:feature-server');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const {CallDirection} = require('./utils/constants');
|
||||
const CallInfo = require('./session/call-info');
|
||||
const retrieveApp = require('./utils/retrieve-app');
|
||||
const parseUrl = require('parse-url');
|
||||
const Requestor = require('./utils/requestor');
|
||||
const makeTask = require('./tasks/make_task');
|
||||
const normalizeJamones = require('./utils/normalize-jamones');
|
||||
|
||||
module.exports = function(srf, logger) {
|
||||
const {lookupAppByPhoneNumber, lookupApplicationBySid} = srf.locals.dbHelpers;
|
||||
@@ -50,12 +50,9 @@ module.exports = function(srf, logger) {
|
||||
const logger = req.locals.logger;
|
||||
try {
|
||||
let app;
|
||||
if (req.locals.application_sid) {
|
||||
app = await lookupApplicationBySid(req.locals.application_sid);
|
||||
}
|
||||
else {
|
||||
app = await lookupAppByPhoneNumber(req.locals.calledNumber);
|
||||
}
|
||||
if (req.locals.application_sid) app = await lookupApplicationBySid(req.locals.application_sid);
|
||||
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`);
|
||||
return res.send(480, {
|
||||
@@ -65,6 +62,14 @@ 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(this.logger, app.call_hook);
|
||||
if (app.call_status_hook) app.notifier = new Requestor(this.logger, app.call_status_hook);
|
||||
else app.notifier = {request: () => {}};
|
||||
|
||||
req.locals.application = app;
|
||||
logger.debug(app, `retrieved application for ${req.locals.calledNumber}`);
|
||||
req.locals.callInfo = new CallInfo({req, app, direction: CallDirection.Inbound});
|
||||
@@ -81,26 +86,13 @@ module.exports = function(srf, logger) {
|
||||
async function invokeWebCallback(req, res, next) {
|
||||
const logger = req.locals.logger;
|
||||
const app = req.locals.application;
|
||||
const call_hook = app.call_hook;
|
||||
const method = call_hook.method.toUpperCase();
|
||||
let auth;
|
||||
if (call_hook.username && call_hook.password) {
|
||||
auth = {username: call_hook.username, password: call_hook.password};
|
||||
}
|
||||
try {
|
||||
const u = parseUrl(call_hook.url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
app.originalRequest = {
|
||||
baseUrl: `${u.protocol}://${u.resource}${myPort}`,
|
||||
auth,
|
||||
method
|
||||
};
|
||||
logger.debug({url: call_hook.url, method}, 'invokeWebCallback');
|
||||
const obj = Object.assign({}, req.locals.callInfo);
|
||||
|
||||
// if the call hook is a POST add the entire SIP message to the payload
|
||||
if (method === 'POST') obj.sip = req.msg;
|
||||
app.tasks = await retrieveApp(logger, call_hook.url, method, auth, obj);
|
||||
/* 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));
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info(`Error retrieving or parsing application: ${err.message}`);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const {CallDirection, CallStatus} = require('../utils/constants');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
/**
|
||||
* @classdesc Represents the common information for all calls
|
||||
* that is provided in call status webhooks
|
||||
*/
|
||||
class CallInfo {
|
||||
constructor(opts) {
|
||||
this.direction = opts.direction;
|
||||
@@ -48,11 +52,20 @@ class CallInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update the status of the call
|
||||
* @param {string} callStatus - current call status
|
||||
* @param {number} sipStatus - current sip status
|
||||
*/
|
||||
updateCallStatus(callStatus, sipStatus) {
|
||||
this.callStatus = callStatus;
|
||||
if (sipStatus) this.sipStatus = sipStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* associate customer-provided data with the call information.
|
||||
* this information will be provided with every call status callhook
|
||||
*/
|
||||
set customerData(obj) {
|
||||
this._customerData = obj;
|
||||
}
|
||||
@@ -84,6 +97,7 @@ class CallInfo {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = CallInfo;
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
const Emitter = require('events');
|
||||
const config = require('config');
|
||||
const {CallDirection, TaskPreconditions, CallStatus, TaskName} = require('../utils/constants');
|
||||
const hooks = require('../utils/notifiers');
|
||||
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 list = require('../utils/summarize-tasks');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
|
||||
/**
|
||||
* @classdesc Represents the execution context for a call.
|
||||
* It holds the resources, such as the sip dialog and media server endpoint
|
||||
* that are needed by Tasks that are operating on the call.<br/><br/>
|
||||
* CallSession is a superclass object that is extended by specific types
|
||||
* of sessions, such as InboundCallSession, RestCallSession and others.
|
||||
*/
|
||||
class CallSession extends Emitter {
|
||||
/**
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {logger} opts.logger - a pino logger
|
||||
* @param {object} opts.application - the application to execute
|
||||
* @param {Srf} opts.srf - the Srf instance
|
||||
* @param {array} opts.tasks - tasks we are to execute
|
||||
* @param {callInfo} opts.callInfo - information about the call
|
||||
*/
|
||||
constructor({logger, application, srf, tasks, callInfo}) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
@@ -16,9 +34,6 @@ class CallSession extends Emitter {
|
||||
this.callInfo = callInfo;
|
||||
this.tasks = tasks;
|
||||
|
||||
const {notifyHook} = hooks(this.logger, this.callInfo);
|
||||
this.notifyHook = notifyHook;
|
||||
|
||||
this.updateCallStatus = srf.locals.dbHelpers.updateCallStatus;
|
||||
this.serviceUrl = srf.locals.serviceUrl;
|
||||
|
||||
@@ -29,65 +44,115 @@ class CallSession extends Emitter {
|
||||
sessionTracker.add(this.callSid, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* callSid for the call being handled by the session
|
||||
*/
|
||||
get callSid() {
|
||||
return this.callInfo.callSid;
|
||||
}
|
||||
|
||||
get originalRequest() {
|
||||
return this.application.originalRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* direction of the call: inbound or outbound
|
||||
*/
|
||||
get direction() {
|
||||
return this.callInfo.direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* SIP call-id for the call
|
||||
*/
|
||||
get callId() {
|
||||
return this.callInfo.direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* http endpoint to send call status updates to
|
||||
*/
|
||||
get call_status_hook() {
|
||||
return this.application.call_status_hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used for all http requests within this session
|
||||
*/
|
||||
get requestor() {
|
||||
assert(this.application.requestor);
|
||||
return this.application.requestor;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used for all http call status notifications within this session
|
||||
*/
|
||||
get notifier() {
|
||||
assert(this.application.notifier);
|
||||
return this.application.notifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* default vendor to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
get speechSynthesisVendor() {
|
||||
return this.application.speech_synthesis_vendor;
|
||||
}
|
||||
/**
|
||||
* default voice to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
get speechSynthesisVoice() {
|
||||
return this.application.speech_synthesis_voice;
|
||||
}
|
||||
|
||||
/**
|
||||
* default vendor to use for speech recognition if not provided in the app
|
||||
*/
|
||||
get speechRecognizerVendor() {
|
||||
return this.application.speech_recognizer_vendor;
|
||||
}
|
||||
/**
|
||||
* default language to use for speech recognition if not provided in the app
|
||||
*/
|
||||
get speechRecognizerLanguage() {
|
||||
return this.application.speech_recognizer_language;
|
||||
}
|
||||
|
||||
/**
|
||||
* indicates whether the call currently in progress
|
||||
*/
|
||||
get hasStableDialog() {
|
||||
return this.dlg && this.dlg.connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* indicates whether call is currently in a ringing state (ie not yet answered)
|
||||
*/
|
||||
get isOutboundCallRinging() {
|
||||
return this.direction === CallDirection.Outbound && this.req && !this.dlg;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the call is an inbound call and a final sip response has been sent
|
||||
*/
|
||||
get isInboundCallAnswered() {
|
||||
return this.direction === CallDirection.Inbound && this.res.finalResponseSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* execute the tasks in the CallSession. The tasks are executed in sequence until
|
||||
* they complete, or the caller hangs up.
|
||||
* @async
|
||||
*/
|
||||
async exec() {
|
||||
this.logger.info(`CallSession:exec starting task list with ${this.tasks.length} tasks`);
|
||||
this.logger.info({tasks: list(this.tasks)}, `CallSession:exec starting ${this.tasks.length} tasks`);
|
||||
while (this.tasks.length && !this.callGone) {
|
||||
const taskNum = ++this.taskIdx;
|
||||
const stackNum = this.stackIdx;
|
||||
const task = this.tasks.shift();
|
||||
this.logger.debug({task}, `CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
try {
|
||||
const resources = await this._evaluatePreconditions(task);
|
||||
this.currentTask = task;
|
||||
await task.exec(this, resources);
|
||||
this.currentTask = null;
|
||||
this.logger.debug(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
} catch (err) {
|
||||
this.currentTask = null;
|
||||
if (err.message.includes(BADPRECONDITIONS)) {
|
||||
@@ -103,16 +168,23 @@ class CallSession extends Emitter {
|
||||
// all done - cleanup
|
||||
this.logger.info('CallSession:exec all tasks complete');
|
||||
this._onTasksDone();
|
||||
this._clearCalls();
|
||||
this.ms && this.ms.destroy();
|
||||
this._clearResources();
|
||||
|
||||
sessionTracker.remove(this.callSid);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when all tasks have completed. It is not implemented in the superclass
|
||||
* but provided as a convenience for subclasses that need to do cleanup at the end of
|
||||
* the call session.
|
||||
*/
|
||||
_onTasksDone() {
|
||||
// meant to be implemented by subclass if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* this is called to clean up when the call is released from one side or another
|
||||
*/
|
||||
_callReleased() {
|
||||
this.logger.debug('CallSession:_callReleased - caller hung up');
|
||||
this.callGone = true;
|
||||
@@ -122,25 +194,12 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
normalizeUrl(url, method, auth) {
|
||||
const hook = {url, method};
|
||||
if (auth && auth.username && auth.password) Object.assign(hook, auth);
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
const or = this.originalRequest;
|
||||
if (or) {
|
||||
hook.url = `${or.baseUrl}${url}`;
|
||||
hook.method = hook.method || or.method || 'POST';
|
||||
if (!hook.auth && or.auth) Object.assign(hook, or.auth);
|
||||
}
|
||||
}
|
||||
this.logger.debug({hook}, 'Task:normalizeUrl');
|
||||
return hook;
|
||||
}
|
||||
|
||||
async updateCall(opts) {
|
||||
this.logger.debug(opts, 'CallSession:updateCall');
|
||||
|
||||
/**
|
||||
* perform live call control - update call status
|
||||
* @param {obj} opts
|
||||
* @param {string} opts.call_status - 'complete' or 'no-answer'
|
||||
*/
|
||||
_lccCallStatus(opts) {
|
||||
if (opts.call_status === CallStatus.Completed && this.dlg) {
|
||||
this.logger.info('CallSession:updateCall hanging up call due to request from api');
|
||||
this._callerHungup();
|
||||
@@ -159,25 +218,139 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (opts.call_hook && opts.call_hook.url) {
|
||||
const hook = this.normalizeUrl(opts.call_hook.url, opts.call_hook.method, opts.call_hook.auth);
|
||||
this.logger.info({hook}, 'CallSession:updateCall replacing application due to request from api');
|
||||
const {actionHook} = hooks(this.logger, this.callInfo);
|
||||
if (opts.call_status_hook) this.call_status_hook = opts.call_status_hook;
|
||||
const tasks = await actionHook(hook);
|
||||
this.logger.info({tasks}, 'CallSession:updateCall new task list');
|
||||
this.replaceApplication(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- set a new call_hook
|
||||
* @param {object} opts
|
||||
* @param {object} opts.call_hook - new call_hook to transfer to
|
||||
* @param {object} [opts.call_hook] - new call_status_hook
|
||||
*/
|
||||
async _lccCallHook(opts) {
|
||||
const tasks = await this.requestor(opts.call_hook, this.callInfo);
|
||||
|
||||
//TODO: if they gave us a call status hook, we should replace
|
||||
//the existing one (or just remove this option altogether?)
|
||||
|
||||
this.logger.info({tasks}, 'CallSession:updateCall new task list');
|
||||
this.replaceApplication(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- change listen status
|
||||
* @param {object} opts
|
||||
* @param {string} opts.listen_status - 'pause' or 'resume'
|
||||
*/
|
||||
async _lccListenStatus(opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || ![TaskName.Dial, TaskName.Listen].includes(task.name)) {
|
||||
return this.logger.info(`CallSession:updateCall - invalid listen_status in task ${task.name}`);
|
||||
}
|
||||
else if (opts.listen_status) {
|
||||
const task = this.currentTask;
|
||||
if (!task || ![TaskName.Dial, TaskName.Listen].includes(task.name)) {
|
||||
return this.logger.info(`CallSession:updateCall - disregarding listen_status in task ${task.name}`);
|
||||
}
|
||||
const listenTask = task.name === TaskName.Listen ? task : task.listenTask;
|
||||
if (!listenTask) {
|
||||
return this.logger.info('CallSession:updateCall - disregarding listen_status as Dial does not have a listen');
|
||||
}
|
||||
listenTask.updateListen(opts.listen_status);
|
||||
const listenTask = task.name === TaskName.Listen ? task : task.listenTask;
|
||||
if (!listenTask) {
|
||||
return this.logger.info('CallSession:updateCall - invalid listen_status: Dial does not have a listen');
|
||||
}
|
||||
listenTask.updateListen(opts.listen_status);
|
||||
}
|
||||
|
||||
async _lccMuteStatus(callSid, mute) {
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Dial !== task.name) {
|
||||
return this.logger.info('CallSession:_lccMute - invalid command as dial is not active');
|
||||
}
|
||||
// now do the whisper
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- whisper to one party or the other on a call
|
||||
* @param {array} opts - array of play or say tasks
|
||||
*/
|
||||
async _lccWhisper(opts, callSid) {
|
||||
const {whisper} = opts;
|
||||
let tasks;
|
||||
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Dial !== task.name) {
|
||||
return this.logger.info('CallSession:_lccWhisper - invalid command since we are not in a dial');
|
||||
}
|
||||
|
||||
// allow user to provide a url object, a url string, an array of tasks, or a single task
|
||||
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));
|
||||
}
|
||||
else if (Array.isArray(whisper)) {
|
||||
// an inline array of tasks
|
||||
tasks = normalizeJamones(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));
|
||||
}
|
||||
else {
|
||||
this.logger.info({opts}, 'CallSession:_lccWhisper invalid options were provided');
|
||||
return;
|
||||
}
|
||||
this.logger.debug(`CallSession:_lccWhisper got ${tasks.length} tasks`);
|
||||
|
||||
// only say or play allowed
|
||||
if (tasks.find((t) => ![TaskName.Say, TaskName.Play].includes(t.name))) {
|
||||
this.logger.info('CallSession:_lccWhisper invalid options where provided');
|
||||
return;
|
||||
}
|
||||
|
||||
//multiple loops not allowed
|
||||
tasks.forEach((t) => t.loop = 1);
|
||||
|
||||
// now do the whisper
|
||||
this.logger.debug(`CallSession:_lccWhisper executing ${tasks.length} tasks`);
|
||||
task.whisper(tasks, callSid).catch((err) => this.logger.error(err, 'CallSession:_lccWhisper'));
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- mute or unmute an endpoint
|
||||
* @param {array} opts - array of play or say tasks
|
||||
*/
|
||||
async _lccMute(callSid, mute) {
|
||||
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Dial !== task.name) {
|
||||
return this.logger.info('CallSession:_lccMute - not possible since we are not in a dial');
|
||||
}
|
||||
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control
|
||||
* @param {object} opts - update instructions
|
||||
* @param {string} callSid - identifies call toupdate
|
||||
*/
|
||||
async updateCall(opts, callSid) {
|
||||
this.logger.debug(opts, 'CallSession:updateCall');
|
||||
|
||||
if (opts.call_status) {
|
||||
return this._lccCallStatus(opts);
|
||||
}
|
||||
if (opts.call_hook) {
|
||||
return await this._lccCallHook(opts);
|
||||
}
|
||||
if (opts.listen_status) {
|
||||
await this._lccListenStatus(opts);
|
||||
}
|
||||
else if (opts.mute_status) {
|
||||
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
|
||||
}
|
||||
|
||||
// whisper may be the only thing we are asked to do, or it may that
|
||||
// we are doing a whisper after having muted, paused reccording etc..
|
||||
if (opts.whisper) {
|
||||
return this._lccWhisper(opts, callSid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +359,10 @@ class CallSession extends Emitter {
|
||||
* NB: any tasks in the current stack that have not been executed are flushed
|
||||
*/
|
||||
replaceApplication(tasks) {
|
||||
if (this.callGone) {
|
||||
this.logger.debug('CallSession:replaceApplication - ignoring because call is gone');
|
||||
return;
|
||||
}
|
||||
this.tasks = tasks;
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx++;
|
||||
@@ -211,6 +388,10 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure call state so as to make a media endpoint available
|
||||
* @param {Task} task - task to be executed
|
||||
*/
|
||||
async _evalEndpointPrecondition(task) {
|
||||
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
|
||||
@@ -229,10 +410,11 @@ class CallSession extends Emitter {
|
||||
|
||||
// we are going from an early media connection to answer
|
||||
await answerCall();
|
||||
return this.ep;
|
||||
}
|
||||
|
||||
// need to allocate an endpoint
|
||||
try {
|
||||
// need to allocate an endpoint
|
||||
if (!this.ms) this.ms = await this.getMS();
|
||||
const ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
ep.cs = this;
|
||||
@@ -256,12 +438,20 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure call state so as to make a sip dialog available
|
||||
* @param {Task} task - task to be executed
|
||||
*/
|
||||
_evalStableCallPrecondition(task) {
|
||||
if (this.callGone) throw new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
if (!this.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered`);
|
||||
return this.dlg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if call has already been answered
|
||||
* @param {Task} task - task to be executed
|
||||
*/
|
||||
_evalUnansweredCallPrecondition(task, callSid) {
|
||||
if (!this.req) throw new Error('invalid precondition unanswered_call for outbound call');
|
||||
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
@@ -271,15 +461,30 @@ class CallSession extends Emitter {
|
||||
return {req: this.req, res: this.res};
|
||||
}
|
||||
|
||||
_clearCalls() {
|
||||
if (this.dlg && this.dlg.connected) this.dlg.destroy();
|
||||
if (this.ep && this.ep.connected) this.ep.destroy();
|
||||
/**
|
||||
* Hang up the call and free the media endpoint
|
||||
*/
|
||||
async _clearResources() {
|
||||
for (const resource of [this.dlg, this.ep, this.ms]) {
|
||||
try {
|
||||
if (resource && resource.connected) await resource.destroy();
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'CallSession:_clearResources error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* called when the caller has hung up. Provided for subclasses to override
|
||||
* in order to apply logic at this point if needed.
|
||||
*/
|
||||
_callerHungup() {
|
||||
assert(false, 'subclass responsibility to override this method');
|
||||
}
|
||||
|
||||
/**
|
||||
* get a media server to use for this call
|
||||
*/
|
||||
async getMS() {
|
||||
if (!this.ms) {
|
||||
const mrf = this.srf.locals.mrf;
|
||||
@@ -288,6 +493,10 @@ class CallSession extends Emitter {
|
||||
return this.ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* create and endpoint if we don't have one; otherwise simply return
|
||||
* the current media server and endpoint that are associated with this call
|
||||
*/
|
||||
async createOrRetrieveEpAndMs() {
|
||||
const mrf = this.srf.locals.mrf;
|
||||
if (this.ms && this.ep) return {ms: this.ms, ep: this.ep};
|
||||
@@ -302,16 +511,24 @@ class CallSession extends Emitter {
|
||||
return {ms: this.ms, ep: this.ep};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called any time call status changes. This method both invokes the
|
||||
* call_status_hook callback as well as updates the realtime database
|
||||
* with latest call status
|
||||
* @param {object} opts
|
||||
* @param {string} callStatus - current call status
|
||||
* @param {number} sipStatus - current sip status
|
||||
* @param {number} [duration] - duration of a completed call, in seconds
|
||||
*/
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
|
||||
const call_status_hook = this.call_status_hook;
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
if (call_status_hook) this.notifyHook(call_status_hook);
|
||||
this.notifier.request(this.call_status_hook, this.callInfo);
|
||||
} catch (err) {
|
||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
const CallSession = require('./call-session');
|
||||
|
||||
/**
|
||||
* @classdesc Subclass of CallSession. Represents a CallSession
|
||||
* that is established for a dial verb that has a
|
||||
* 'confirmUrl' application that is executed upon call answer.
|
||||
* @extends CallSession
|
||||
|
||||
*/
|
||||
class ConfirmCallSession extends CallSession {
|
||||
constructor({logger, application, dlg, ep, tasks, callInfo}) {
|
||||
super({
|
||||
@@ -17,7 +24,7 @@ class ConfirmCallSession extends CallSession {
|
||||
/**
|
||||
* empty implementation to override superclass so we do not delete dlg and ep
|
||||
*/
|
||||
_clearCalls() {
|
||||
_clearResources() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ const {CallStatus} = require('../utils/constants');
|
||||
const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
|
||||
/**
|
||||
* @classdesc Subclass of CallSession. This represents a CallSession that is
|
||||
* established for an inbound call.
|
||||
* @extends CallSession
|
||||
*/
|
||||
class InboundCallSession extends CallSession {
|
||||
constructor(req, res) {
|
||||
super({
|
||||
@@ -34,46 +39,9 @@ class InboundCallSession extends CallSession {
|
||||
}
|
||||
}
|
||||
|
||||
async connectInboundCallToIvr(earlyMedia = false) {
|
||||
|
||||
// check for a stable inbound call already connected to the ivr
|
||||
if (this.ep && this.dlg) {
|
||||
this.logger.debug('CallSession:connectInboundCallToIvr - inbound call already connected to IVR');
|
||||
return {ep: this.ep, dlg: this.dlg};
|
||||
}
|
||||
|
||||
// check for an early media connection, where caller wants same
|
||||
if (this.ep && earlyMedia) {
|
||||
this.logger.debug('CallSession:connectInboundCallToIvr - inbound call already has early media connection');
|
||||
return {ep: this.ep};
|
||||
}
|
||||
|
||||
// ok, we need to connect the inbound call to the ivr
|
||||
try {
|
||||
assert(!this.req.finalResponseSent);
|
||||
this.logger.debug('CallSession:connectInboundCallToIvr - creating endpoint for inbound call');
|
||||
const {ep} = await this.createOrRetrieveEpAndMs();
|
||||
this.ep = ep;
|
||||
|
||||
if (earlyMedia) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
this.emit('callStatusChange', {sipStatus: 183, callStatus: CallStatus.EarlyMedia});
|
||||
return {ep, res: this.res};
|
||||
}
|
||||
const dlg = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
|
||||
dlg.on('destroy', this._callerHungup.bind(this));
|
||||
dlg.connectTime = moment();
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug(`CallSession:connectInboundCallToIvr - answered callSid ${this.callSid}`);
|
||||
this.ep = ep;
|
||||
this.dlg = dlg;
|
||||
return {ep, dlg};
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'CallSession:connectInboundCallToIvr error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer the call, if it has not already been answered.
|
||||
*/
|
||||
async propagateAnswer() {
|
||||
if (!this.dlg) {
|
||||
assert(this.ep);
|
||||
@@ -85,6 +53,9 @@ class InboundCallSession extends CallSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is invoked when the caller hangs up, in order to calculate the call duration.
|
||||
*/
|
||||
_callerHungup() {
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
@@ -92,7 +63,6 @@ class InboundCallSession extends CallSession {
|
||||
this.logger.debug('InboundCallSession: caller hung up');
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = InboundCallSession;
|
||||
|
||||
@@ -2,6 +2,11 @@ const CallSession = require('./call-session');
|
||||
const {CallStatus} = require('../utils/constants');
|
||||
const moment = require('moment');
|
||||
|
||||
/**
|
||||
* @classdesc Subclass of CallSession. This represents a CallSession that is
|
||||
* created for an outbound call that is initiated via the REST API.
|
||||
* @extends CallSession
|
||||
*/
|
||||
class RestCallSession extends CallSession {
|
||||
constructor({logger, application, srf, req, ep, tasks, callInfo}) {
|
||||
super({
|
||||
@@ -19,12 +24,19 @@ class RestCallSession extends CallSession {
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the sip dialog that is created when the far end answers.
|
||||
* @param {Dialog} dlg - sip dialog
|
||||
*/
|
||||
setDialog(dlg) {
|
||||
this.dlg = dlg;
|
||||
dlg.on('destroy', this._callerHungup.bind(this));
|
||||
dlg.connectTime = moment();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is invoked when the called party hangs up, in order to calculate the call duration.
|
||||
*/
|
||||
_callerHungup() {
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
const Emitter = require('events');
|
||||
const assert = require('assert');
|
||||
|
||||
/**
|
||||
* @classdesc This is a singleton class that tracks active sessions in a Map indexed
|
||||
* by callSid. Its function is to allow us to accept inbound REST callUpdate requests
|
||||
* for a callSid and to be able to retrieve and operate on the corresponding CallSession.
|
||||
*/
|
||||
class SessionTracker extends Emitter {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -16,22 +21,39 @@ class SessionTracker extends Emitter {
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new CallSession to the Map
|
||||
* @param {string} callSid
|
||||
* @param {CallSession} callSession
|
||||
*/
|
||||
add(callSid, callSession) {
|
||||
assert(callSid);
|
||||
this.sessions.set(callSid, callSession);
|
||||
this.logger.info(`SessionTracker:add callSid ${callSid}, we have ${this.sessions.size} session being tracked`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a CallSession from the Map
|
||||
* @param {string} callSid
|
||||
*/
|
||||
remove(callSid) {
|
||||
assert(callSid);
|
||||
this.sessions.delete(callSid);
|
||||
this.logger.info(`SessionTracker:remove callSid ${callSid}, we have ${this.sessions.size} being tracked`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given callSid is in the Map
|
||||
* @param {string} callSid
|
||||
*/
|
||||
has(callSid) {
|
||||
return this.sessions.has(callSid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the active CallSession for a given callSid
|
||||
* @param {string} callSid
|
||||
*/
|
||||
get(callSid) {
|
||||
return this.sessions.get(callSid);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,37 @@ const makeTask = require('./make_task');
|
||||
const {CallStatus, CallDirection, TaskName, TaskPreconditions, MAX_SIMRINGS} = require('../utils/constants');
|
||||
const assert = require('assert');
|
||||
const placeCall = require('../utils/place-outdial');
|
||||
const sessionTracker = require('../session/session-tracker');
|
||||
const DtmfCollector = require('../utils/dtmf-collector');
|
||||
const config = require('config');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
|
||||
function parseDtmfOptions(logger, dtmfCapture) {
|
||||
let parentDtmfCollector, childDtmfCollector;
|
||||
const parentKeys = [], childKeys = [];
|
||||
|
||||
if (Array.isArray(dtmfCapture)) {
|
||||
Array.prototype.push.apply(parentKeys, dtmfCapture);
|
||||
Array.prototype.push.apply(childKeys, dtmfCapture);
|
||||
}
|
||||
else if (dtmfCapture.childCall || dtmfCapture.parentCall) {
|
||||
if (dtmfCapture.childCall && Array.isArray(dtmfCapture.childCall)) {
|
||||
Array.prototype.push.apply(childKeys, dtmfCapture.childCall);
|
||||
}
|
||||
if (dtmfCapture.parentCall && Array.isArray(dtmfCapture.parentCall)) {
|
||||
Array.prototype.push.apply(childKeys, dtmfCapture.parentCall);
|
||||
}
|
||||
}
|
||||
if (childKeys.length) {
|
||||
childDtmfCollector = new DtmfCollector({logger, patterns: childKeys});
|
||||
}
|
||||
if (parentKeys.length) {
|
||||
parentDtmfCollector = new DtmfCollector({logger, patterns: parentKeys});
|
||||
}
|
||||
|
||||
return {childDtmfCollector, parentDtmfCollector};
|
||||
}
|
||||
|
||||
function compareTasks(t1, t2) {
|
||||
if (t1.type !== t2.type) return false;
|
||||
switch (t1.type) {
|
||||
@@ -44,6 +72,7 @@ class TaskDial extends Task {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.None;
|
||||
|
||||
this.actionHook = this.data.actionHook;
|
||||
this.earlyMedia = this.data.answerOnBridge === true;
|
||||
this.callerId = this.data.callerId;
|
||||
this.dialMusic = this.data.dialMusic;
|
||||
@@ -52,8 +81,19 @@ class TaskDial extends Task {
|
||||
this.target = filterAndLimit(this.logger, this.data.target);
|
||||
this.timeout = this.data.timeout || 60;
|
||||
this.timeLimit = this.data.timeLimit;
|
||||
this.confirmUrl = this.data.confirmUrl;
|
||||
this.confirmHook = this.data.confirmHook;
|
||||
this.confirmMethod = this.data.confirmMethod;
|
||||
this.dtmfHook = this.data.dtmfHook;
|
||||
|
||||
if (this.dtmfHook) {
|
||||
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
|
||||
if (parentDtmfCollector) {
|
||||
this.parentDtmfCollector = parentDtmfCollector;
|
||||
}
|
||||
if (childDtmfCollector) {
|
||||
this.childDtmfCollector = childDtmfCollector;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.data.listen) {
|
||||
this.listenTask = makeTask(logger, {'listen': this.data.listen}, this);
|
||||
@@ -83,9 +123,15 @@ class TaskDial extends Task {
|
||||
if (cs.direction === CallDirection.Inbound) {
|
||||
await this._initializeInbound(cs);
|
||||
}
|
||||
else {
|
||||
this.epOther = cs.ep;
|
||||
}
|
||||
this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
|
||||
await this._attemptCalls(cs);
|
||||
await this.awaitTaskDone();
|
||||
await this.performAction(this.method, null, this.results);
|
||||
await cs.requestor.request(this.actionHook, Object.assign({}, cs.callInfo, this.results));
|
||||
this._removeDtmfDetection(cs, this.epOther);
|
||||
this._removeDtmfDetection(cs, this.ep);
|
||||
} catch (err) {
|
||||
this.logger.error(`TaskDial:exec terminating with error ${err.message}`);
|
||||
this.kill();
|
||||
@@ -94,17 +140,57 @@ class TaskDial extends Task {
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
this._removeDtmfDetection(this.cs, this.epOther);
|
||||
this._removeDtmfDetection(this.cs, this.ep);
|
||||
this._killOutdials();
|
||||
if (this.sd) {
|
||||
this.sd.kill();
|
||||
this.sd = null;
|
||||
}
|
||||
sessionTracker.remove(this.callSid);
|
||||
if (this.listenTask) await this.listenTask.kill();
|
||||
if (this.transcribeTask) await this.transcribeTask.kill();
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* whisper a prompt to one side of the call
|
||||
* @param {*} tasks - array of play/say tasks to execute
|
||||
*/
|
||||
async whisper(tasks, callSid) {
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
|
||||
try {
|
||||
const cs = this.callSession;
|
||||
this.logger.debug('Dial:whisper unbridging endpoints');
|
||||
await this.epOther.unbridge();
|
||||
this.logger.debug('Dial:whisper executing tasks');
|
||||
while (tasks.length && !cs.callGone) {
|
||||
const task = tasks.shift();
|
||||
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
|
||||
}
|
||||
this.logger.debug('Dial:whisper tasks complete');
|
||||
if (!cs.callGone) this.epOther.bridge(this.ep);
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Dial:whisper error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mute or unmute one side of the call
|
||||
*/
|
||||
async mute(callSid, doMute) {
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:mute: no paired endpoint found');
|
||||
try {
|
||||
const parentCall = callSid !== this.callSid;
|
||||
const ep = parentCall ? this.epOther : this.ep;
|
||||
await ep[doMute ? 'mute' : 'unmute']();
|
||||
this.logger.debug(`Dial:mute ${doMute ? 'muted' : 'unmuted'} ${parentCall ? 'parentCall' : 'childCall'}`);
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Dial:mute error');
|
||||
}
|
||||
}
|
||||
|
||||
_killOutdials() {
|
||||
for (const [callSid, sd] of Array.from(this.dials)) {
|
||||
this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`);
|
||||
@@ -113,8 +199,33 @@ class TaskDial extends Task {
|
||||
this.dials.clear();
|
||||
}
|
||||
|
||||
_installDtmfDetection(cs, ep, dtmfDetector) {
|
||||
if (ep && this.dtmfHook && !ep.dtmfDetector) {
|
||||
ep.dtmfDetector = dtmfDetector;
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
}
|
||||
_removeDtmfDetection(cs, ep) {
|
||||
if (ep) {
|
||||
delete ep.dtmfDetector;
|
||||
ep.removeListener('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
const match = ep.dtmfDetector.keyPress(evt.dtmf);
|
||||
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
|
||||
cs.requestor :
|
||||
this.sd.requestor;
|
||||
if (match) {
|
||||
this.logger.debug(`parentCall triggered dtmf match: ${match}`);
|
||||
requestor.request(this.dtmfHook, Object.assign({dtmf: match}, cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
|
||||
async _initializeInbound(cs) {
|
||||
const {ep} = await cs.connectInboundCallToIvr(this.earlyMedia);
|
||||
const ep = await cs._evalEndpointPrecondition(this);
|
||||
this.epOther = ep;
|
||||
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
|
||||
|
||||
@@ -256,8 +367,10 @@ class TaskDial extends Task {
|
||||
this.kill();
|
||||
}, this.timeLimit * 1000);
|
||||
}
|
||||
sessionTracker.add(this.callSid, cs);
|
||||
this.dlg.on('destroy', () => {
|
||||
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
|
||||
sessionTracker.remove(this.callSid);
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.ep.unbridge();
|
||||
this.kill();
|
||||
@@ -268,6 +381,8 @@ class TaskDial extends Task {
|
||||
dialCallSid: sd.callSid,
|
||||
});
|
||||
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.ep, this.childDtmfCollector);
|
||||
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.ep);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@ class TaskGather extends Task {
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'action', 'finishOnKey', 'hints', 'input', 'method', 'numDigits',
|
||||
'partialResultCallback', 'partialResultCallbackMethod', 'profanityFilter',
|
||||
'finishOnKey', 'hints', 'input', 'numDigits',
|
||||
'partialResultHook', 'profanityFilter',
|
||||
'speechTimeout', 'timeout', 'say', 'play'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
|
||||
this.partialResultCallbackMethod = this.partialResultCallbackMethod || 'POST';
|
||||
this.method = this.method || 'POST';
|
||||
this.timeout = (this.timeout || 5) * 1000;
|
||||
this.interim = this.partialResultCallback;
|
||||
if (this.data.recognizer) {
|
||||
@@ -23,7 +21,6 @@ class TaskGather extends Task {
|
||||
this.vendor = this.data.recognizer.vendor;
|
||||
}
|
||||
|
||||
|
||||
this.digitBuffer = '';
|
||||
this._earlyMedia = this.data.earlyMedia === true;
|
||||
|
||||
@@ -142,7 +139,10 @@ class TaskGather extends Task {
|
||||
_onTranscription(ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if (evt.is_final) this._resolve('speech', evt);
|
||||
else if (this.partialResultCallback) this.notifyHook(this.partialResultCallback, 'POST', null, {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'));
|
||||
}
|
||||
}
|
||||
_onEndOfUtterance(ep, evt) {
|
||||
this.logger.info(evt, 'TaskGather:_onEndOfUtterance');
|
||||
@@ -154,10 +154,10 @@ class TaskGather extends Task {
|
||||
|
||||
this._clearTimer();
|
||||
if (reason.startsWith('dtmf')) {
|
||||
await this.performAction(this.method, null, {digits: this.digitBuffer});
|
||||
await this.performAction({digits: this.digitBuffer});
|
||||
}
|
||||
else if (reason.startsWith('speech')) {
|
||||
await this.performAction(this.method, null, {speech: evt});
|
||||
await this.performAction({speech: evt});
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ class TaskListen extends Task {
|
||||
this.nested = parentTask instanceof Task;
|
||||
|
||||
this.results = {};
|
||||
this.ranToCompletion = false;
|
||||
|
||||
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
|
||||
|
||||
@@ -42,8 +41,7 @@ class TaskListen extends Task {
|
||||
}
|
||||
await this._startListening(cs, ep);
|
||||
await this.awaitTaskDone();
|
||||
const acceptNewApp = !this.nested && this.ranToCompletion;
|
||||
if (this.action) await this.performAction(this.method, this.auth, this.results, acceptNewApp);
|
||||
await this.performAction(this.results, !this.nested);
|
||||
} catch (err) {
|
||||
this.logger.info(err, `TaskListen:exec - error ${this.url}`);
|
||||
}
|
||||
@@ -68,18 +66,15 @@ class TaskListen extends Task {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
updateListen(status) {
|
||||
async updateListen(status) {
|
||||
if (!this.killed && this.ep && this.ep.connected) {
|
||||
this.logger.info(`TaskListen:updateListen status ${status}`);
|
||||
switch (status) {
|
||||
case ListenStatus.Pause:
|
||||
this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
||||
break;
|
||||
case ListenStatus.Silence:
|
||||
this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
||||
await this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
||||
break;
|
||||
case ListenStatus.Resume:
|
||||
this.ep.forkAudioResume().catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
||||
await this.ep.forkAudioResume().catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -114,7 +109,6 @@ 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.ranToCompletion = true;
|
||||
this.kill();
|
||||
}, this.maxLength * 1000);
|
||||
}
|
||||
@@ -142,7 +136,6 @@ class TaskListen extends Task {
|
||||
if (evt.dtmf === this.finishOnKey) {
|
||||
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
||||
this.results.digits = evt.dtmf;
|
||||
this.ranToCompletion = true;
|
||||
this.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,13 @@ const {TaskName} = require('../utils/constants');
|
||||
class TaskRedirect extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
|
||||
this.action = this.data.action;
|
||||
this.method = (this.data.method || 'POST').toUpperCase();
|
||||
this.auth = this.data.auth;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Redirect; }
|
||||
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
await this.performAction(this.method, this.auth);
|
||||
await this.performAction();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,7 @@ class TaskRestDial extends Task {
|
||||
this.req = null;
|
||||
const cs = this.callSession;
|
||||
cs.setDialog(dlg);
|
||||
const obj = Object.assign({}, cs.callInfo);
|
||||
|
||||
const tasks = await this.actionHook(this.call_hook, obj);
|
||||
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(tasks);
|
||||
|
||||
@@ -39,11 +39,11 @@
|
||||
},
|
||||
"gather": {
|
||||
"properties": {
|
||||
"action": "string",
|
||||
"actionHook": "object|string",
|
||||
"finishOnKey": "string",
|
||||
"input": "array",
|
||||
"numDigits": "number",
|
||||
"partialResultCallback": "string",
|
||||
"partialResultHook": "object|string",
|
||||
"speechTimeout": "number",
|
||||
"timeout": "number",
|
||||
"recognizer": "#recognizer",
|
||||
@@ -51,26 +51,20 @@
|
||||
"say": "#say"
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"dial": {
|
||||
"properties": {
|
||||
"action": "string",
|
||||
"actionHook": "object|string",
|
||||
"answerOnBridge": "boolean",
|
||||
"callerId": "string",
|
||||
"confirmUrl": "string",
|
||||
"confirmMethod": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"confirmHook": "object|string",
|
||||
"dialMusic": "string",
|
||||
"dtmfCapture": "object",
|
||||
"dtmfHook": "object|string",
|
||||
"headers": "object",
|
||||
"listen": "#listen",
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"target": ["#target"],
|
||||
"timeLimit": "number",
|
||||
"timeout": "number",
|
||||
@@ -82,15 +76,11 @@
|
||||
},
|
||||
"listen": {
|
||||
"properties": {
|
||||
"action": "string",
|
||||
"actionHook": "object|string",
|
||||
"auth": "#auth",
|
||||
"finishOnKey": "string",
|
||||
"maxLength": "number",
|
||||
"metadata": "object",
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"mixType": {
|
||||
"type": "string",
|
||||
"enum": ["mono", "stereo", "mixed"]
|
||||
@@ -118,21 +108,17 @@
|
||||
},
|
||||
"redirect": {
|
||||
"properties": {
|
||||
"action": "string",
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"auth": "#auth"
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"rest:dial": {
|
||||
"properties": {
|
||||
"call_hook": "object",
|
||||
"call_hook": "object|string",
|
||||
"from": "string",
|
||||
"tag": "object",
|
||||
"to": "#target",
|
||||
"timeout": "number"
|
||||
},
|
||||
@@ -152,12 +138,12 @@
|
||||
},
|
||||
"transcribe": {
|
||||
"properties": {
|
||||
"transcriptionCallback": "string",
|
||||
"transcriptionHook": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"transcriptionCallback"
|
||||
"transcriptionHook"
|
||||
]
|
||||
},
|
||||
"target": {
|
||||
|
||||
@@ -2,25 +2,39 @@ 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 makeTask = require('./make_task');
|
||||
const specs = new Map();
|
||||
const _specData = require('./specs');
|
||||
for (const key in _specData) {specs.set(key, _specData[key]);}
|
||||
|
||||
/**
|
||||
* @classdesc Represents a jambonz verb. This is a superclass that is extended
|
||||
* by a subclass for each verb.
|
||||
* @extends Emitter
|
||||
*/
|
||||
class Task extends Emitter {
|
||||
constructor(logger, data) {
|
||||
super();
|
||||
this.preconditions = TaskPreconditions.None;
|
||||
this.logger = logger;
|
||||
this.data = data;
|
||||
this.actionHook = this.data.actionHook;
|
||||
|
||||
this._killInProgress = false;
|
||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {boolean} killed - true if the task has been killed
|
||||
*/
|
||||
get killed() {
|
||||
return this._killInProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {CallSession} callSession - the CallSession this task is executing within
|
||||
*/
|
||||
get callSession() {
|
||||
return this.cs;
|
||||
}
|
||||
@@ -29,13 +43,13 @@ class Task extends Emitter {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the task. Subclasses must implement this method, but should always call
|
||||
* the superclass implementation first.
|
||||
* @param {CallSession} cs - the CallSession that the Task will be executing within.
|
||||
*/
|
||||
async exec(cs) {
|
||||
this.cs = cs;
|
||||
|
||||
// N.B. need to require it down here rather than at top to avoid recursion in require of this module
|
||||
const {actionHook, notifyHook} = require('../utils/notifiers')(this.logger, cs.callInfo);
|
||||
this.actionHook = actionHook;
|
||||
this.notifyHook = notifyHook;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,29 +62,47 @@ class Task extends Emitter {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* when a subclass Task has completed its work, it should call this method
|
||||
*/
|
||||
notifyTaskDone() {
|
||||
this._completionResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* when a subclass task has launched various async activities and is now simply waiting
|
||||
* for them to complete it should call this method to block until that happens
|
||||
*/
|
||||
awaitTaskDone() {
|
||||
return this._completionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* provided as a convenience for tasks, this simply calls CallSession#normalizeUrl
|
||||
*/
|
||||
normalizeUrl(url, method, auth) {
|
||||
return this.callSession.normalizeUrl(url, method, auth);
|
||||
}
|
||||
|
||||
async performAction(method, auth, results, expectResponse = true) {
|
||||
if (this.action) {
|
||||
const hook = this.normalizeUrl(this.action, method, auth);
|
||||
const tasks = await this.actionHook(hook, results, expectResponse);
|
||||
if (expectResponse && tasks && Array.isArray(tasks)) {
|
||||
this.logger.debug({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.callSession.replaceApplication(tasks);
|
||||
async performAction(results, expectResponse = true) {
|
||||
if (this.actionHook) {
|
||||
const params = results ? Object.assign(results, this.cs.callInfo) : this.cs.callInfo;
|
||||
const json = await this.cs.requestor.request(this.actionHook, params);
|
||||
if (expectResponse && json && Array.isArray(json)) {
|
||||
const tasks = normalizeJamones(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that the JSON task description is valid
|
||||
* @param {string} name - verb name
|
||||
* @param {object} data - verb properties
|
||||
*/
|
||||
static validate(name, data) {
|
||||
debug(`validating ${name} with data ${JSON.stringify(data)}`);
|
||||
// validate the instruction is supported
|
||||
@@ -94,6 +126,12 @@ class Task extends Emitter {
|
||||
else if (typeof dSpec === 'string' && dSpec === 'array') {
|
||||
if (!Array.isArray(dVal)) throw new Error(`${name}: property ${dKey} is not an array`);
|
||||
}
|
||||
else if (typeof dSpec === 'string' && dSpec.includes('|')) {
|
||||
const types = dSpec.split('|').map((t) => t.trim());
|
||||
if (!types.includes(typeof dVal)) {
|
||||
throw new Error(`${name}: property ${dKey} has invalid data type, must be one of ${types}`);
|
||||
}
|
||||
}
|
||||
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
|
||||
const name = dSpec[0].slice(1);
|
||||
for (const item of dVal) {
|
||||
|
||||
@@ -6,7 +6,7 @@ class TaskTranscribe extends Task {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.transcriptionCallback = this.data.transcriptionCallback;
|
||||
this.transcriptionHook = this.data.transcriptionHook;
|
||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||
if (this.data.recognizer) {
|
||||
this.language = this.data.recognizer.language || 'en-US';
|
||||
@@ -78,7 +78,8 @@ class TaskTranscribe extends Task {
|
||||
|
||||
_onTranscription(ep, evt) {
|
||||
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
|
||||
this.notifyHook(this.transcriptionCallback, 'POST', null, {speech: evt});
|
||||
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
|
||||
if (this.killed) {
|
||||
this.logger.debug('TaskTranscribe:_onTranscription exiting after receiving final transcription');
|
||||
this._clearTimer();
|
||||
|
||||
10
lib/utils/basic-auth.js
Normal file
10
lib/utils/basic-auth.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
|
||||
module.exports = (auth) => {
|
||||
if (!auth || !auth.username ||
|
||||
typeof auth.username !== 'string' ||
|
||||
(auth.password && typeof auth.password !== 'string')) return {};
|
||||
const creds = `${auth.username}:${auth.password || ''}`;
|
||||
const header = `Basic ${toBase64(creds)}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
42
lib/utils/dtmf-collector.js
Normal file
42
lib/utils/dtmf-collector.js
Normal file
@@ -0,0 +1,42 @@
|
||||
class DtmfEntry {
|
||||
constructor(key, time) {
|
||||
this.key = key;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @classdesc Represents an object that collects dtmf key entries and
|
||||
* reports when a match is detected
|
||||
*/
|
||||
class DtmfCollector {
|
||||
constructor({logger, patterns, interDigitTimeout}) {
|
||||
this.logger = logger;
|
||||
this.patterns = patterns;
|
||||
this.idt = interDigitTimeout || 3000;
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
keyPress(key) {
|
||||
const now = Date.now();
|
||||
|
||||
// age out previous entries if interdigit timer has elapsed
|
||||
const lastDtmf = this.buffer.pop();
|
||||
if (lastDtmf) {
|
||||
if (now - lastDtmf.time < this.idt) this.buffer.push(lastDtmf);
|
||||
else {
|
||||
this.buffer = [];
|
||||
}
|
||||
}
|
||||
// add new entry
|
||||
this.buffer.push(new DtmfEntry(key, now));
|
||||
|
||||
// check for a match
|
||||
const collectedDigits = this.buffer
|
||||
.map((entry) => entry.key)
|
||||
.join('');
|
||||
return this.patterns.find((pattern) => collectedDigits.endsWith(pattern));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DtmfCollector;
|
||||
@@ -1,52 +0,0 @@
|
||||
const request = require('request');
|
||||
//require('request-debug')(request);
|
||||
const retrieveApp = require('./retrieve-app');
|
||||
|
||||
function hooks(logger, callInfo) {
|
||||
function actionHook(hook, obj = {}, expectResponse = true) {
|
||||
const method = (hook.method || 'POST').toUpperCase();
|
||||
const auth = (hook.username && hook.password) ?
|
||||
{username: hook.username, password: hook.password} :
|
||||
null;
|
||||
|
||||
const data = Object.assign({}, obj, callInfo.toJSON());
|
||||
logger.debug({hook, data, auth}, 'actionhook');
|
||||
|
||||
/* customer data only on POSTs */
|
||||
if ('GET' === method) delete data.customerData;
|
||||
|
||||
const opts = {
|
||||
url: hook.url,
|
||||
method,
|
||||
json: 'POST' === method || expectResponse
|
||||
};
|
||||
if (auth) opts.auth = auth;
|
||||
if ('POST' === method) opts.body = data;
|
||||
else opts.qs = data;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(opts, (err, response, body) => {
|
||||
if (err) {
|
||||
logger.info(`actionHook error ${method} ${hook.url}: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
if (body && expectResponse) {
|
||||
logger.debug(body, `actionHook response ${method} ${hook.url}`);
|
||||
return resolve(retrieveApp(logger, body));
|
||||
}
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function notifyHook(hook, opts = {}) {
|
||||
return actionHook(hook, opts, false);
|
||||
}
|
||||
|
||||
return {
|
||||
actionHook,
|
||||
notifyHook
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = hooks;
|
||||
@@ -5,9 +5,7 @@ const {TaskPreconditions, CallDirection} = require('../utils/constants');
|
||||
const CallInfo = require('../session/call-info');
|
||||
const assert = require('assert');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const hooks = require('./notifiers');
|
||||
const moment = require('moment');
|
||||
const parseUrl = require('parse-url');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
@@ -20,8 +18,7 @@ class SingleDialer extends Emitter {
|
||||
this.sbcAddress = sbcAddress;
|
||||
this.opts = opts;
|
||||
this.application = application;
|
||||
this.url = target.url;
|
||||
this.method = target.method;
|
||||
this.confirmHook = target.confirmHook;
|
||||
|
||||
this.bindings = logger.bindings();
|
||||
|
||||
@@ -37,6 +34,22 @@ class SingleDialer extends Emitter {
|
||||
return this.callInfo.callStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used for all http requests within this session
|
||||
*/
|
||||
get requestor() {
|
||||
assert(this.application.requestor);
|
||||
return this.application.requestor;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used for all http call status notifications within this session
|
||||
*/
|
||||
get notifier() {
|
||||
assert(this.application.notifier);
|
||||
return this.application.notifier;
|
||||
}
|
||||
|
||||
async exec(srf, ms, opts) {
|
||||
let uri, to;
|
||||
switch (this.target.type) {
|
||||
@@ -106,9 +119,6 @@ class SingleDialer extends Emitter {
|
||||
callId: this.callInfo.callId
|
||||
});
|
||||
this.inviteInProgress = req;
|
||||
const {actionHook, notifyHook} = hooks(this.logger, this.callInfo);
|
||||
this.actionHook = actionHook;
|
||||
this.notifyHook = notifyHook;
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
},
|
||||
cbProvisional: (prov) => {
|
||||
@@ -135,7 +145,7 @@ class SingleDialer extends Emitter {
|
||||
this.ep.destroy();
|
||||
});
|
||||
|
||||
if (this.url) this._executeApp(this.url);
|
||||
if (this.confirmHook) this._executeApp(this.confirmHook);
|
||||
else this.emit('accept');
|
||||
} catch (err) {
|
||||
const status = {callStatus: CallStatus.Failed};
|
||||
@@ -178,29 +188,12 @@ class SingleDialer extends Emitter {
|
||||
* Note: the application to run may not include a dial or sip:decline verb
|
||||
* @param {*} url - url for application
|
||||
*/
|
||||
async _executeApp(url) {
|
||||
this.logger.debug(`SingleDialer:_executeApp: executing ${url} after connect`);
|
||||
async _executeApp(confirmHook) {
|
||||
try {
|
||||
let auth, method;
|
||||
const app = Object.assign({}, this.application);
|
||||
if (url.startsWith('/')) {
|
||||
const savedUrl = url;
|
||||
const or = app.originalRequest;
|
||||
url = `${or.baseUrl}${url}`;
|
||||
auth = or.auth;
|
||||
method = this.method || or.method || 'POST';
|
||||
this.logger.debug({originalUrl: savedUrl, normalizedUrl: url}, 'SingleDialer:_executeApp normalized url');
|
||||
}
|
||||
else {
|
||||
const u = parseUrl(url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
app.originalRequest = {
|
||||
baseUrl: `${u.protocol}://${u.resource}${myPort}`
|
||||
};
|
||||
method = this.method || 'POST';
|
||||
}
|
||||
// retrieve set of tasks
|
||||
const tasks = await this.requestor.request(confirmHook, this.callInfo);
|
||||
|
||||
const tasks = await this.actionHook({url, method, auth});
|
||||
// verify it contains only allowed verbs
|
||||
const allowedTasks = tasks.filter((task) => {
|
||||
return [
|
||||
TaskPreconditions.StableCall,
|
||||
@@ -211,16 +204,19 @@ class SingleDialer extends Emitter {
|
||||
throw new Error('unsupported verb in dial url');
|
||||
}
|
||||
|
||||
// now execute it in a new ConfirmCallSession
|
||||
this.logger.debug(`SingleDialer:_executeApp: executing ${tasks.length} tasks`);
|
||||
const cs = new ConfirmCallSession({
|
||||
logger: this.logger,
|
||||
application: app,
|
||||
application: this.application,
|
||||
dlg: this.dlg,
|
||||
ep: this.ep,
|
||||
callInfo: this.callInfo,
|
||||
tasks
|
||||
});
|
||||
await cs.exec();
|
||||
|
||||
// still connected after app is completed? Signal parent call we are good
|
||||
this.emit(this.dlg.connected ? 'accept' : 'decline');
|
||||
} catch (err) {
|
||||
this.logger.debug(err, 'SingleDialer:_executeApp: error');
|
||||
@@ -233,6 +229,7 @@ class SingleDialer extends Emitter {
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
|
||||
64
lib/utils/requestor.js
Normal file
64
lib/utils/requestor.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const bent = require('bent');
|
||||
const parseUrl = require('parse-url');
|
||||
const basicAuth = require('./basic-auth');
|
||||
const assert = require('assert');
|
||||
|
||||
function isRelativeUrl(u) {
|
||||
return typeof u === 'string' && u.startsWith('/');
|
||||
}
|
||||
|
||||
function isAbsoluteUrl(u) {
|
||||
return typeof u === 'string' &&
|
||||
u.startsWith('https://') || u.startsWith('http://');
|
||||
}
|
||||
|
||||
class Requestor {
|
||||
constructor(logger, hook) {
|
||||
this.logger = logger;
|
||||
this.url = hook.url;
|
||||
this.method = hook.method || 'POST';
|
||||
this.authHeader = basicAuth(hook.auth);
|
||||
|
||||
const u = parseUrl(this.url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
const baseUrl = `${u.protocol}://${u.resource}${myPort}`;
|
||||
|
||||
this.get = bent(baseUrl, 'GET', 'json', 200);
|
||||
this.post = bent(baseUrl, 'POST', 'json', 200);
|
||||
|
||||
assert(isAbsoluteUrl(this.url));
|
||||
assert(['GET', 'POST'].includes(this.method));
|
||||
assert(!this.auth || typeof auth == 'object');
|
||||
}
|
||||
|
||||
get hasAuth() {
|
||||
return 'Authorization' in this.authHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {object} [params] - request parameters
|
||||
*/
|
||||
async request(hook, params) {
|
||||
params = params || null;
|
||||
if (isRelativeUrl(hook)) {
|
||||
this.logger.debug({params}, `Requestor:request relative url ${hook}`);
|
||||
return await this.post(hook, params, this.authHeader);
|
||||
}
|
||||
const url = hook.url;
|
||||
const method = hook.method || 'POST';
|
||||
const authHeader = isRelativeUrl(url) ? this.authHeader : basicAuth(hook.auth);
|
||||
|
||||
assert(url);
|
||||
assert(['GET', 'POST'].includes(method));
|
||||
return await this[method.toLowerCase()](url, params, authHeader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Requestor;
|
||||
@@ -1,31 +0,0 @@
|
||||
const request = require('request');
|
||||
//require('request-debug')(request);
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const normalizeJamones = require('./normalize-jamones');
|
||||
|
||||
|
||||
function retrieveUrl(logger, url, method, auth, obj) {
|
||||
const opts = {url, method, auth, json: true};
|
||||
if (method === 'GET') Object.assign(opts, {qs: obj});
|
||||
else Object.assign(opts, {body: obj});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(opts, (err, response, body) => {
|
||||
if (err) throw err;
|
||||
if (response.statusCode === 401) return reject(new Error('HTTP request failed: Unauthorized'));
|
||||
else if (response.statusCode !== 200) return reject(new Error(`HTTP request failed: ${response.statusCode}`));
|
||||
if (body) logger.debug({body}, 'retrieveUrl: ${method} ${url} returned an application');
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function retrieveApp(logger, url, method, auth, obj) {
|
||||
let json;
|
||||
|
||||
if (typeof url === 'object') json = url;
|
||||
else json = await retrieveUrl(logger, url, method, auth, obj);
|
||||
return normalizeJamones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
}
|
||||
|
||||
module.exports = retrieveApp;
|
||||
3
lib/utils/summarize-tasks.js
Normal file
3
lib/utils/summarize-tasks.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = function(tasks) {
|
||||
return `[${tasks.map((t) => t.name).join(',')}]`;
|
||||
};
|
||||
229
package-lock.json
generated
229
package-lock.json
generated
@@ -295,6 +295,23 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"bent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bent/-/bent-7.0.6.tgz",
|
||||
"integrity": "sha512-lDSus5exz6HwJIpqf+aQxqYqA0Xrrca9w3INpqduP1ojlI9bHAURvSn/tzK4cQ0TRB8tt6rvOzIBNFyJvBRqnw==",
|
||||
"requires": {
|
||||
"bytesish": "^0.4.1",
|
||||
"caseless": "~0.12.0",
|
||||
"is-stream": "^2.0.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=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
|
||||
@@ -400,6 +417,11 @@
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
||||
},
|
||||
"bytesish": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.1.tgz",
|
||||
"integrity": "sha512-j3l5QmnAbpOfcN/Z2Jcv4poQYfefs8rDdcbc6iEKm+OolvUXAE2APodpWj+DOzqX6Bl5Ys1cQkcIV2/doGvQxg=="
|
||||
},
|
||||
"caching-transform": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz",
|
||||
@@ -438,6 +460,15 @@
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"catharsis": {
|
||||
"version": "0.8.11",
|
||||
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz",
|
||||
"integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@@ -812,50 +843,6 @@
|
||||
"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.13",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-1.5.13.tgz",
|
||||
"integrity": "sha512-FC/Xifua4ut5tZ9cDRCaRoEIo7LEevh5gdqgzTyKo685gm10tO//Ln7Q6ZnVnbwpFOH4TxaIf+al25z/t0v6Cg==",
|
||||
"requires": {
|
||||
"async": "^1.4.2",
|
||||
"debug": "^2.2.0",
|
||||
"delegates": "^0.1.0",
|
||||
"drachtio-modesl": "^1.2.0",
|
||||
"drachtio-srf": "^4.4.15",
|
||||
"lodash": "^4.17.15",
|
||||
"minimist": "^1.2.0",
|
||||
"moment": "^2.13.0",
|
||||
"only": "0.0.2",
|
||||
"sdp-transform": "^2.7.0",
|
||||
"uuid": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
}
|
||||
}
|
||||
},
|
||||
"drachtio-modesl": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-modesl/-/drachtio-modesl-1.2.0.tgz",
|
||||
"integrity": "sha512-nkua3RfYnT32OvglERO2zWzJZAfQooZIarZVVAye+iGqTwYJ69X7bU7du5SBHz/jBl+LgeWITMP2fMe2TelxmA==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"eventemitter2": "^4.1",
|
||||
"uuid": "^3.1.0",
|
||||
"xml2js": "^0.4.19"
|
||||
}
|
||||
},
|
||||
"drachtio-mw-registration-parser": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-mw-registration-parser/-/drachtio-mw-registration-parser-0.0.2.tgz",
|
||||
@@ -933,6 +920,12 @@
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"entities": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
|
||||
"dev": true
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -1129,11 +1122,6 @@
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||
},
|
||||
"eventemitter2": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz",
|
||||
"integrity": "sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU="
|
||||
},
|
||||
"events-to-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz",
|
||||
@@ -2096,11 +2084,50 @@
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"js2xmlparser": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz",
|
||||
"integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"xmlcreate": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
|
||||
},
|
||||
"jsdoc": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.3.tgz",
|
||||
"integrity": "sha512-Yf1ZKA3r9nvtMWHO1kEuMZTlHOF8uoQ0vyo5eH7SQy5YeIiHM+B0DgKnn+X6y6KDYZcF7G2SPkKF+JORCXWE/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.4.4",
|
||||
"bluebird": "^3.5.4",
|
||||
"catharsis": "^0.8.11",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"js2xmlparser": "^4.0.0",
|
||||
"klaw": "^3.0.0",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it-anchor": "^5.0.2",
|
||||
"marked": "^0.7.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"requizzle": "^0.2.3",
|
||||
"strip-json-comments": "^3.0.1",
|
||||
"taffydb": "2.6.2",
|
||||
"underscore": "~1.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
@@ -2153,6 +2180,15 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"klaw": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
|
||||
"integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.9"
|
||||
}
|
||||
},
|
||||
"lcov-parse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz",
|
||||
@@ -2169,6 +2205,15 @@
|
||||
"type-check": "~0.3.2"
|
||||
}
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"load-json-file": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
|
||||
@@ -2263,6 +2308,37 @@
|
||||
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
|
||||
"dev": true
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
|
||||
"integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~1.1.1",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"markdown-it-anchor": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz",
|
||||
"integrity": "sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ==",
|
||||
"dev": true
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
|
||||
"dev": true
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=",
|
||||
"dev": true
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -3136,6 +3212,15 @@
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true
|
||||
},
|
||||
"requizzle": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
|
||||
"integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz",
|
||||
@@ -3207,16 +3292,6 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"sdp-transform": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.0.tgz",
|
||||
"integrity": "sha512-8ZYOau/o9PzRhY0aMuRzvmiM6/YVQR8yjnBScvZHSdBnywK5oZzAJK+412ZKkDq29naBmR3bRw8MFu0C01Gehg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
@@ -3622,6 +3697,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"taffydb": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
|
||||
"integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=",
|
||||
"dev": true
|
||||
},
|
||||
"tap": {
|
||||
"version": "14.10.2",
|
||||
"resolved": "https://registry.npmjs.org/tap/-/tap-14.10.2.tgz",
|
||||
@@ -5221,6 +5302,12 @@
|
||||
"integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
|
||||
"dev": true
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.2.tgz",
|
||||
@@ -5241,6 +5328,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz",
|
||||
"integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==",
|
||||
"dev": true
|
||||
},
|
||||
"unicode-length": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/unicode-length/-/unicode-length-1.0.3.tgz",
|
||||
@@ -5429,19 +5522,11 @@
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
"xmlcreate": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz",
|
||||
"integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==",
|
||||
"dev": true
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
|
||||
@@ -26,10 +26,11 @@
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"bent": "^7.0.6",
|
||||
"config": "^3.2.4",
|
||||
"debug": "^4.1.1",
|
||||
"drachtio-fn-b2b-sugar": "0.0.12",
|
||||
"drachtio-fsmrf": "^1.5.13",
|
||||
"drachtio-fsmrf": "^1.5.14xs",
|
||||
"drachtio-srf": "^4.4.27",
|
||||
"express": "^4.17.1",
|
||||
"ip": "^1.1.5",
|
||||
@@ -37,15 +38,14 @@
|
||||
"jambonz-realtimedb-helpers": "0.1.6",
|
||||
"moment": "^2.24.0",
|
||||
"parse-url": "^5.0.1",
|
||||
"pino": "^5.14.0",
|
||||
"request": "^2.88.0",
|
||||
"request-debug": "^0.2.0"
|
||||
"pino": "^5.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"blue-tape": "^1.0.0",
|
||||
"clear-module": "^4.0.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"jsdoc": "^3.6.3",
|
||||
"nyc": "^14.1.1",
|
||||
"tap": "^14.10.2",
|
||||
"tap-dot": "^2.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dial": {
|
||||
"action": "http://example.com",
|
||||
"actionHook": "http://example.com",
|
||||
"callerId": "+1312888899",
|
||||
"target": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dial": {
|
||||
"action": "http://example.com",
|
||||
"actionHook": "http://example.com",
|
||||
"callerId": "+1312888899",
|
||||
"target": [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"verb": "gather",
|
||||
"action": "https://00dd977a.ngrok.io/gather",
|
||||
"actionHook": "https://00dd977a.ngrok.io/gather",
|
||||
"input": ["speech"],
|
||||
"timeout": 12,
|
||||
"recognizer": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dial": {
|
||||
"action": "http://example.com",
|
||||
"actionHook": "http://example.com",
|
||||
"callerId": "+1312888899",
|
||||
"target": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dial": {
|
||||
"action": "http://example.com",
|
||||
"actionHook": "http://example.com",
|
||||
"callerId": "+1312888899",
|
||||
"target": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dial": {
|
||||
"action": "http://example.com",
|
||||
"actionHook": "http://example.com",
|
||||
"callerId": "+1312888899",
|
||||
"target": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dial": {
|
||||
"action": "http://example.com",
|
||||
"actionHook": "http://example.com",
|
||||
"callerId": "+1312888899",
|
||||
"target": [
|
||||
{
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
],
|
||||
"transcribe": {
|
||||
"transcriptionCallback": "http://example.com/transcribe",
|
||||
"transcriptionHook": "/transcribe",
|
||||
"recognizer": {
|
||||
"vendor": "google",
|
||||
"language" : "en-US",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dial": {
|
||||
"action": "http://example.com",
|
||||
"actionHook": "http://example.com",
|
||||
"callerId": "+1312888899",
|
||||
"target": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user