initial work on dial verb

This commit is contained in:
Dave Horton
2020-01-07 20:57:49 -05:00
parent 523e2a308b
commit 1debb193c2
8 changed files with 309 additions and 22 deletions

3
app.js
View File

@@ -1,5 +1,7 @@
const Srf = require('drachtio-srf');
const srf = new Srf();
const Mrf = require('drachtio-fsmrf');
srf.locals.mrf = new Mrf(srf);
const config = require('config');
const logger = require('pino')(config.get('logging'));
const {lookupAppByPhoneNumber} = require('jambonz-db-helpers')(config.get('mysql'), logger);
@@ -10,6 +12,7 @@ const {
retrieveApplication,
invokeWebCallback
} = require('./lib/middleware')(srf, logger);
const CallSession = require('./lib/call-session');
// disable logging in test mode

View File

@@ -3,6 +3,13 @@
"port": 3010,
"secret": "cymru"
},
"freeswitch: [
{
"address": "127.0.0.1",
"port": 8021,
"secret": "ClueCon"
}
],
"logging": {
"level": "info"
},

View File

@@ -1,11 +1,6 @@
const Emitter = require('events');
/*
const config = require('config');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {parseUri, SipError} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-inbound');
const assert = require('assert');
*/
class CallSession extends Emitter {
constructor(req, res) {
super();
@@ -14,6 +9,11 @@ class CallSession extends Emitter {
this.srf = req.srf;
this.logger = req.locals.logger;
this.application = req.locals.application;
this.resources = new Map();
req.on('cancel', this._onCallerHangup.bind(this));
this.on('callStatusChange', this._onCallStatusChange.bind(this));
}
async exec() {
@@ -26,13 +26,69 @@ class CallSession extends Emitter {
} catch (err) {
this.logger.error({err, task}, 'Error executing task');
}
this.logger.info('finished all tasks');
this.logger.info('CallSession: finished all tasks');
if (!this.res.finalResponseSent) {
this.logger.info('auto-generating non-success response to invite');
this.logger.info('CallSession: auto-generating non-success response to invite');
this.res.send(603);
}
this._clearResources();
}
}
addResource(name, resource) {
this.logger.debug(`CallSession:addResource: adding ${name}`);
this.resources.set(name, resource);
}
getResource(name) {
return this.resources.get(name);
}
removeResource(name) {
this.logger.debug(`CallSession:removeResource: removing ${name}`);
this.resources.delete(name);
}
async createOrRetrieveEpAndMs(remoteSdp) {
const mrf = this.srf.locals.mrf;
let ms = this.getResource('ms');
let ep = this.getResource('epIn');
if (ms && ep) return {ms, ep};
// get a media server
if (!ms) {
ms = await mrf.connect(config.get('freeswitch'));
this.addResource('ms', ms);
}
if (!ep) {
ep = await ms.createEndpoint({remoteSdp});
this.addResource('epIn', ep);
}
return {ms, ep};
}
/**
* clear down resources
* (note: we remove in reverse order they were added since mediaserver
* is typically added first and I prefer to destroy it after any resources it holds)
*/
_clearResources() {
for (const [name, resource] of Array.from(this.resources).reverse()) {
try {
this.logger.info(`CallSession:_clearResources: deleting ${name}`);
if (resource.connected) resource.destroy();
} catch (err) {
this.logger.error(err, `CallSession:_clearResources: error deleting ${name}`);
}
}
}
_onCallerHangup(evt) {
this.logger.debug('CallSession: caller hung before connection');
}
_onCallStatusChange(evt) {
this.logger.debug(evt, 'CallSession:_onCallStatusChange');
}
}
module.exports = CallSession;

View File

@@ -63,17 +63,33 @@ module.exports = function(srf, logger) {
*/
async function invokeWebCallback(req, res, next) {
const logger = req.locals.logger;
const app = req.locals.application;
const call_sid = uuidv4();
const account_sid = req.locals.application.account_sid;
const application_sid = req.locals.application.application_sid;
const method = (app.hook_http_method || 'GET').toUpperCase();
const from = req.getParsedHeader('From');
const opts = {
url: app.call_hook,
method,
json: true,
qs: {
CallSid: call_sid,
AccountSid: app.account_sid,
From: req.callingNumber,
To: req.calledNumber,
CallStatus: 'ringing',
Direction: 'inbound',
CallerName: from.name || req.callingNumber
}
};
if (app.hook_basic_auth_user && app.hook_basic_auth_password) {
Object.assign(opts, {auth: {user: app.hook_basic_auth_user, password: app.hook_basic_auth_password}});
}
if (method === 'POST') {
Object.assign(opts, {json: true, body: req.msg});
}
try {
const app = req.locals.application;
assert(app && app.call_hook);
request.post({
url: app.call_hook,
json: true,
body: req.msg
}, (err, response, body) => {
request(opts, (err, response, body) => {
if (err) {
logger.error(err, `Error invoking callback ${app.call_hook}`);
return res.send(603, 'Bad webhook');

View File

@@ -1,11 +1,30 @@
const Task = require('./task');
const name = 'dial';
const makeTask = require('./make_task');
const assert = require('assert');
class TaskDial extends Task {
constructor(logger, opts) {
super(logger, opts);
this.name = name;
this.headers = this.data.headers || {};
this.answerOnBridge = opts.answerOnBridge === true;
this.timeout = opts.timeout || 60;
this.method = opts.method || 'GET';
this.dialMusic = opts.dialMusic;
this.timeLimit = opts.timeLimit;
this.strategy = opts.strategy || 'hunt';
this.target = opts.target;
this.canceled = false;
this.finished = false;
this.localResources = {};
if (opts.transcribe) {
this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe});
}
if (opts.listen) {
this.listenTask = makeTask(logger, {'listen': opts.transcribe});
}
}
static get name() { return name; }
@@ -14,12 +33,109 @@ class TaskDial extends Task {
* Reject an incoming call attempt with a provided status code and (optionally) reason
*/
async exec(cs) {
if (!cs.res.finalResponseSent) {
cs.res.send(this.data.status, this.data.reason, {
headers: this.headers
});
try {
await this._initializeInbound(cs);
//await connectCall(cs);
await this._untilCallEnds(cs);
} catch (err) {
this.logger.info(`TaskDial:exec terminating with error ${err.message}`);
}
return false;
return true;
}
async _initializeInbound(cs) {
const {req, res} = cs;
// the caller could hangup in the middle of all this..
req.on('cancel', this._onCancel.bind(this, cs));
try {
const {ep} = await cs.createOrRetrieveEpAndMs(req.body);
// caller might have hung up while we were doing that
if (this.canceled) throw new Error('caller hung up');
// check if inbound leg has already been answered
let uas = cs.getResource('dlgIn');
if (!uas) {
// if answerOnBridge send a 183 (early media), otherwise go ahead and answer the call
if (this.answerOnBridge && !req.finalResponseSent) {
if (!this.dialMusic) res.send(180);
else {
res.send(183, {body: ep.remote.sdp});
}
}
else {
uas = await cs.srf.createUAS(req, res, {localSdp: ep.local.sdp});
cs.addResource('dlgIn', uas);
uas.on('destroy', this._onCallerHangup.bind(this, cs, uas));
}
cs.emit('callStatusChange', {status: 'ringing'});
}
// play dial music to caller, if provided
if (this.dialMusic) {
ep.play(this.dialMusic, (err) => {
if (err) this.logger.error(err, `TaskDial:_initializeInbound - error playing ${this.dialMusic}`);
});
}
} catch (err) {
this.logger.error(err, 'TaskDial:_initializeInbound error');
this.finished = true;
if (!res.finalResponseSent && !this.canceled) res.send(500);
this._clearResources(cs);
throw err;
}
}
_clearResources(cs) {
for (const key in this.localResources) {
this.localResources[key].destroy();
}
this.localResources = {};
}
_onCancel(cs) {
this.logger.info('TaskDial: caller hung up before connecting');
this.canceled = this.finished = true;
this._clearResources();
cs.emit('callStatusChange', {status: 'canceled'});
}
_onCallerHangup(cs, dlg) {
cs.emit('callStatusChange', {status: 'canceled'});
this.finished = true;
this._clearResources();
}
/**
* returns a Promise that resolves when the call ends
*/
_untilCallEnds(cs) {
const {res} = cs;
return new Promise((resolve) => {
assert(!this.finished);
//TMP - hang up in 5 secs
setTimeout(() => {
res.send(480);
this._clearResources();
resolve();
}, 5000);
//TMP
/*
const dlgOut = this.localResources.dlgOut;
assert(dlgIn.connected && dlgOut.connected);
[this.dlgIn, this.dlgOut].forEach((dlg) => {
dlg.on('destroy', () => resolve());
});
*/
});
}
}

View File

@@ -1,11 +1,13 @@
const Emitter = require('events');
const debug = require('debug')('jambonz:feature-server');
const assert = require('assert');
const specs = new Map();
const _specData = require('./specs');
for (const key in _specData) {specs.set(key, _specData[key]);}
class Task {
class Task extends Emitter {
constructor(logger, data) {
super();
this.logger = logger;
this.data = data;
}

86
package-lock.json generated
View File

@@ -706,6 +706,58 @@
"resolved": "https://registry.npmjs.org/drachtio-fn-b2b-sugar/-/drachtio-fn-b2b-sugar-0.0.12.tgz",
"integrity": "sha512-FKPAcEMJTYKDrd9DJUCc4VHnY/c65HOO9k8XqVNognF9T02hKEjGuBCM4Da9ipyfiHmVRuECwj0XNvZ361mkVQ=="
},
"drachtio-fn-fsmrf-sugar": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/drachtio-fn-fsmrf-sugar/-/drachtio-fn-fsmrf-sugar-0.0.9.tgz",
"integrity": "sha512-X32AUmURLaeXmMQrY05YmkevAYpT0CIuoEsoiKqkZviSSt88e55naJf8xrWsMP7W5rc4UuYBmgCiBqW3qKoR9Q==",
"requires": {
"drachtio-fsmrf": "^1.4.4"
}
},
"drachtio-fsmrf": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-1.5.8.tgz",
"integrity": "sha512-hVSJuGiwX/onshvtmKyjA1p/wiPmezuiblT5Z3YGSz7tPPK5e+m4fIwAjEvarQSfhrMeiXuQHp9Eewp0+z0J0Q==",
"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",
@@ -959,6 +1011,11 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
"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",
@@ -2025,6 +2082,11 @@
}
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -2757,6 +2819,16 @@
"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.13.0",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.13.0.tgz",
"integrity": "sha512-3zT7pcjR090E0WCV9eOtFX06iojoNKsyMXqXs7clOs8sy+RoegR0cebmCuCrTKdY2jw1XhT9jkraygJrqAUwzA=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -4882,6 +4954,20 @@
"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=="
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -28,6 +28,7 @@
"config": "^3.2.4",
"debug": "^4.1.1",
"drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-fsmrf": "1.5.10",
"drachtio-srf": "^4.4.27",
"jambonz-db-helpers": "^0.1.6",
"pino": "^5.14.0",