Compare commits

...

15 Commits

Author SHA1 Message Date
Dave Horton
48e860112b add ability to have custom headers on outdial 2020-04-15 11:14:30 -04:00
Dave Horton
5f1eead24f refine prev checkin 2020-04-15 11:08:28 -04:00
Dave Horton
592b0fa2aa bugfix: when createCall fails to create a leg, it also generated an unhandled exception 2020-04-15 11:03:32 -04:00
Dave Horton
12b6f58a0d bugfix for #15: dialMusic not played on A leg when direction is outbound 2020-04-15 08:19:08 -04:00
Dave Horton
aa9e781baf refactor answer logic to one location 2020-04-14 09:39:48 -04:00
Dave Horton
50011e01dd bugfix #14 - incorrect from when PAI differs from From header 2020-04-06 11:31:30 -04:00
Dave Horton
74404c155f bugfix #13, rest outdial is properly canceled after timeout 2020-04-06 10:46:47 -04:00
Dave Horton
a3c077586f fix freeswitch retry connection logic 2020-03-30 10:30:24 -04:00
Dave Horton
ed838ffa28 fix exception in dial verb when dtmf detected but detector not in place 2020-03-30 09:17:00 -04:00
Dave Horton
e23573e833 fix error with createCall api 2020-03-23 15:00:12 -04:00
Dave Horton
7581eca524 prevent calling dlg.destroy twice 2020-03-09 22:17:44 +00:00
Dave Horton
ff8b6c6b18 check dlg connected before deleting 2020-03-09 22:07:30 +00:00
Dave Horton
52f790836a remove dup completed call status 2020-03-09 22:06:10 +00:00
Dave Horton
017b3a66a8 address general case of sending completed status when we hangup the call 2020-03-09 21:55:41 +00:00
Dave Horton
d82e2254ab send callStatus completed event when REST call is ended with BYE generated from jambonz (#4) 2020-03-08 12:51:49 +00:00
9 changed files with 103 additions and 72 deletions

6
app.js
View File

@@ -29,7 +29,11 @@ const {
// HTTP
const express = require('express');
const app = express();
app.locals.logger = logger;
Object.assign(app.locals, {
logger,
srf
});
const httpRoutes = require('./lib/http-routes');
const InboundCallSession = require('./lib/session/inbound-call-session');

View File

@@ -7,29 +7,21 @@ const SipError = require('drachtio-srf').SipError;
const sysError = require('./error');
const Requestor = require('../../utils/requestor');
/**
* Retrieve a connection to a drachtio server, lazily creating when first called
*/
function getSrfForOutdial(logger) {
const {srf} = require('../../../');
const {getSrf} = srf.locals;
const srfForOutdial = getSrf();
if (!srfForOutdial) throw new Error('no available feature servers for outbound call creation');
return srfForOutdial;
}
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const {logger, srf} = req.app.locals;
logger.debug({body: req.body}, 'got createCall request');
try {
let uri, cs, to;
const restDial = makeTask(logger, {'rest:dial': req.body});
const srf = getSrfForOutdial(logger);
const {getSBC, getFreeswitch} = srf.locals;
const sbcAddress = getSBC();
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
const target = restDial.to;
const opts = { callingNumber: restDial.from };
const opts = {
callingNumber: restDial.from,
headers: req.body.headers || {}
};
switch (target.type) {
case 'phone':
@@ -90,9 +82,9 @@ router.post('/', async(req, res) => {
if (err) {
logger.error(err, 'createCall Error creating call');
res.status(500).send('Call Failure');
ep.destroy();
return;
}
/* ok our outbound NVITE is in flight */
/* ok our outbound INVITE is in flight */
const tasks = [restDial];
const callInfo = new CallInfo({

View File

@@ -1,4 +1,5 @@
const {CallDirection, CallStatus} = require('../utils/constants');
const parseUri = require('drachtio-srf').parseUri;
const uuidv4 = require('uuid/v4');
/**
@@ -7,16 +8,22 @@ const uuidv4 = require('uuid/v4');
*/
class CallInfo {
constructor(opts) {
let from ;
this.direction = opts.direction;
if (opts.req) {
const u = opts.req.getParsedHeader('from');
const uri = parseUri(u.uri);
from = uri.user;
this.callerName = u.name || '';
}
if (this.direction === CallDirection.Inbound) {
// inbound call
const {app, req} = opts;
this.callSid = req.locals.callSid,
this.accountSid = app.account_sid,
this.applicationSid = app.application_sid;
this.from = req.callingNumber;
this.from = from || req.callingNumber;
this.to = req.calledNumber;
this.callerName = this.from.name || req.callingNumber;
this.callId = req.get('Call-ID');
this.sipStatus = 100;
this.callStatus = CallStatus.Trying;
@@ -30,7 +37,7 @@ class CallInfo {
this.parentCallSid = parentCallInfo.callSid;
this.accountSid = parentCallInfo.accountSid;
this.applicationSid = parentCallInfo.applicationSid;
this.from = req.callingNumber;
this.from = from || req.callingNumber;
this.to = to;
this.callerId = this.from.name || req.callingNumber;
this.callId = req.get('Call-ID');
@@ -46,7 +53,7 @@ class CallInfo {
this.callStatus = CallStatus.Trying,
this.callId = req.get('Call-ID');
this.sipStatus = 100;
this.from = req.callingNumber;
this.from = from || req.callingNumber;
this.to = to;
if (tag) this._customerData = tag;
}

View File

@@ -421,6 +421,7 @@ class CallSession extends Emitter {
uas.callSid = this.callSid;
uas.connectTime = moment();
this.dlg = uas;
this.wrapDialog(this.dlg);
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
this.logger.debug('CallSession:_evalEndpointPrecondition - answered call');
};
@@ -532,6 +533,57 @@ class CallSession extends Emitter {
return {ms: this.ms, ep: this.ep};
}
/**
* Call this whenever we answer the A leg, creating a dialog
* It wraps the 'destroy' method such that if we hang up the A leg
* (e.g. via 'hangup' verb) we emit a callStatusChange event
* @param {SipDialog} dlg
*/
wrapDialog(dlg) {
dlg.connectTime = moment();
const origDestroy = dlg.destroy.bind(dlg);
dlg.destroy = () => {
if (dlg.connected) {
dlg.connected = false;
dlg.destroy = origDestroy;
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('CallSession: call terminated by jambones');
origDestroy();
}
};
}
/**
* Answer the call, if it has not already been answered.
*
* NB: This should be the one and only place we generate 200 OK to incoming INVITEs
*/
async propagateAnswer() {
if (!this.dlg) {
assert(this.ep);
this.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
this.dlg.on('destroy', this._callerHungup.bind(this));
this.wrapDialog(this.dlg);
this.dlg.callSid = this.callSid;
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
this.dlg.on('modify', this._onReinvite.bind(this));
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
}
}
async _onReinvite(req, res) {
try {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
}
}
/**
* Called any time call status changes. This method both invokes the
* call_status_hook callback as well as updates the realtime database

View File

@@ -31,26 +31,6 @@ class InboundCallSession extends CallSession {
this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite');
this.res.send(603);
}
else if (this.dlg && this.dlg.connected) {
assert(this.dlg.connectTime);
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('InboundCallSession:_onTasksDone hanging up call since all tasks are done');
}
}
/**
* Answer the call, if it has not already been answered.
*/
async propagateAnswer() {
if (!this.dlg) {
assert(this.ep);
this.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
this.dlg.connectTime = moment();
this.dlg.on('destroy', this._callerHungup.bind(this));
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
}
}
/**

View File

@@ -31,7 +31,7 @@ class RestCallSession extends CallSession {
setDialog(dlg) {
this.dlg = dlg;
dlg.on('destroy', this._callerHungup.bind(this));
dlg.connectTime = moment();
this.wrapDialog(dlg);
}
/**
@@ -40,7 +40,7 @@ class RestCallSession extends CallSession {
_callerHungup() {
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('InboundCallSession: caller hung up');
this.logger.debug('RestCallSession: called party hung up');
this._callReleased();
}

View File

@@ -123,6 +123,9 @@ class TaskDial extends Task {
}
else {
this.epOther = cs.ep;
if (this.dialMusic && this.epOther && this.epOther.connected) {
this.epOther.play(this.dialMusic).catch((err) => {});
}
}
this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
await this._attemptCalls(cs);
@@ -206,19 +209,21 @@ class TaskDial extends Task {
_removeDtmfDetection(cs, ep) {
if (ep) {
delete ep.dtmfDetector;
ep.removeListener('dtmf', this._onDtmf.bind(this, cs, ep));
ep.removeAllListeners('dtmf');
}
}
_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'));
if (ep.dtmfDetector) {
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'));
}
}
}
@@ -240,18 +245,6 @@ class TaskDial extends Task {
const {getSBC} = srf.locals;
const sbcAddress = getSBC();
/*
if (CallDirection.Inbound === cs.direction) {
const contact = req.getParsedHeader('Contact');
const uri = parseUri(contact[0].uri);
this.logger.debug({contact}, 'outdialing with contact');
sbcAddress = `${uri.host}:${uri.port || 5060}`;
//sbcAddress = `${req.source_address}:${req.source_port}`;
}
else {
sbcAddress = getSBC();
}
*/
if (!sbcAddress) throw new Error('no SBC found for outbound call');
const opts = {
headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers,

View File

@@ -24,9 +24,9 @@ class TaskRestDial extends Task {
/**
* INVITE has just been sent at this point
*/
async exec(cs, req) {
async exec(cs) {
super.exec(cs);
this.req = req;
this.req = cs.req;
this._setCallTimer();
await this.awaitTaskDone();

View File

@@ -53,8 +53,9 @@ function installSrfLocals(srf, logger) {
// retry to connect to any that were initially offline
setInterval(async() => {
for (const val of mediaservers) {
if (val.connect === 0) {
if (val.connects === 0) {
try {
logger.info({mediaserver: val.opts}, 'Retrying initial connection to media server');
const ms = await mrf.connect(val.opts);
val.ms = ms;
} catch (err) {
@@ -66,13 +67,15 @@ function installSrfLocals(srf, logger) {
// if we have a single freeswitch (as is typical) report stats periodically
if (mediaservers.length === 1) {
const ms = mediaservers[0].ms;
setInterval(() => {
try {
stats.gauge('fs.media.channels.in_use', ms.currentSessions);
stats.gauge('fs.media.channels.free', ms.maxSessions - ms.currentSessions);
stats.gauge('fs.media.calls_per_second', ms.cps);
stats.gauge('fs.media.cpu_idle', ms.cpuIdle);
if (mediaservers[0].ms && mediaservers[0].active) {
const ms = mediaservers[0].ms;
stats.gauge('fs.media.channels.in_use', ms.currentSessions);
stats.gauge('fs.media.channels.free', ms.maxSessions - ms.currentSessions);
stats.gauge('fs.media.calls_per_second', ms.cps);
stats.gauge('fs.media.cpu_idle', ms.cpuIdle);
}
}
catch (err) {
logger.info(err, 'Error sending media server metrics');