mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-19 04:17:44 +00:00
initial work on dial verb
This commit is contained in:
3
app.js
3
app.js
@@ -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
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
"port": 3010,
|
||||
"secret": "cymru"
|
||||
},
|
||||
"freeswitch: [
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"port": 8021,
|
||||
"secret": "ClueCon"
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"level": "info"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
*/
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
86
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user