Feature/incoming refer (#76)

* Dial: handle incoming REFER on either leg by calling referHook, if configured

* lint

* modify payload of referHook

* support target.trunk on rest createCall api

* bugfix: gather partial result hook was not working

* lint

* handling of incoming REFER
This commit is contained in:
Dave Horton
2022-03-05 15:21:26 -05:00
committed by GitHub
parent 9908485eb8
commit 72b74de767
14 changed files with 213 additions and 27 deletions

View File

@@ -72,6 +72,17 @@ router.post('/', async(req, res) => {
break; break;
} }
if (target.type === 'phone' && target.trunk) {
const {lookupCarrier} = dbUtils(this.logger, srf);
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
this.logger.info(
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
if (voip_carrier_sid) {
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
}
}
/* create endpoint for outdial */ /* create endpoint for outdial */
const ms = getFreeswitch(); const ms = getFreeswitch();
if (!ms) throw new Error('no available Freeswitch for outbound call creation'); if (!ms) throw new Error('no available Freeswitch for outbound call creation');
@@ -160,7 +171,11 @@ router.post('/', async(req, res) => {
} }
}); });
connectStream(dlg.remote.sdp); connectStream(dlg.remote.sdp);
cs.emit('callStatusChange', {callStatus: CallStatus.InProgress, sipStatus: 200}); cs.emit('callStatusChange', {
callStatus: CallStatus.InProgress,
sipStatus: 200,
sipReason: 'OK'
});
restDial.emit('callStatus', 200); restDial.emit('callStatus', 200);
restDial.emit('connect', dlg); restDial.emit('connect', dlg);
} }
@@ -171,10 +186,18 @@ router.post('/', async(req, res) => {
else if (487 === err.status) callStatus = CallStatus.NoAnswer; else if (487 === err.status) callStatus = CallStatus.NoAnswer;
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`); if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
else console.log(`REST outdial failed with ${err.status}`); else console.log(`REST outdial failed with ${err.status}`);
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: err.status}); if (cs) cs.emit('callStatusChange', {
callStatus,
sipStatus: err.status,
sipReason: err.reason
});
} }
else { else {
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: 500}); if (cs) cs.emit('callStatusChange', {
callStatus,
sipStatus: 500,
sipReason: 'Internal Server Error'
});
if (sipLogger) sipLogger.error({err}, 'REST outdial failed'); if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
else console.error(err); else console.error(err);
} }

View File

@@ -12,6 +12,9 @@ function retrieveCallSession(callSid, opts) {
throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated'); throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated');
} }
const cs = sessionTracker.get(callSid); const cs = sessionTracker.get(callSid);
if (!cs) {
throw new DbErrorUnprocessableRequest('call session is gone');
}
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) { if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action'); throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
@@ -45,8 +48,18 @@ router.post('/:callSid', async(req, res) => {
logger.info(`updateCall: callSid not found ${callSid}`); logger.info(`updateCall: callSid not found ${callSid}`);
return res.sendStatus(404); return res.sendStatus(404);
} }
res.sendStatus(202);
cs.updateCall(req.body, callSid); if (req.body.sip_request) {
const response = await cs.updateCall(req.body, callSid);
res.status(200).json({
status: response.status,
reason: response.reason
});
}
else {
res.sendStatus(202);
cs.updateCall(req.body, callSid);
}
} catch (err) { } catch (err) {
sysError(logger, res, err); sysError(logger, res, err);
} }

View File

@@ -30,15 +30,25 @@ class AdultingCallSession extends CallSession {
return this.sd.dlg; return this.sd.dlg;
} }
/**
* Note: this is not an error. It is only here to avoid an assert ("no setter for dlg")
* when there is a call in Session:_clearResources to null out dlg and ep
*/
set dlg(newDlg) {}
get ep() { get ep() {
return this.sd.ep; return this.sd.ep;
} }
/* see note above */
set ep(newEp) {}
get callSid() { get callSid() {
return this.callInfo.callSid; return this.callInfo.callSid;
} }
_callerHungup() {
}
} }
module.exports = AdultingCallSession; module.exports = AdultingCallSession;

View File

@@ -27,6 +27,7 @@ class CallInfo {
this.to = req.calledNumber; this.to = req.calledNumber;
this.callId = req.get('Call-ID'); this.callId = req.get('Call-ID');
this.sipStatus = 100; this.sipStatus = 100;
this.sipReason = 'Trying';
this.callStatus = CallStatus.Trying; this.callStatus = CallStatus.Trying;
this.originatingSipIp = req.get('X-Forwarded-For'); this.originatingSipIp = req.get('X-Forwarded-For');
this.originatingSipTrunkName = req.get('X-Originating-Carrier'); this.originatingSipTrunkName = req.get('X-Originating-Carrier');
@@ -45,6 +46,7 @@ class CallInfo {
this.callId = req.get('Call-ID'); this.callId = req.get('Call-ID');
this.callStatus = CallStatus.Trying, this.callStatus = CallStatus.Trying,
this.sipStatus = 100; this.sipStatus = 100;
this.sipReason = 'Trying';
} }
else if (this.direction === CallDirection.None) { else if (this.direction === CallDirection.None) {
// outbound SMS // outbound SMS
@@ -65,6 +67,7 @@ class CallInfo {
this.callStatus = CallStatus.Trying, this.callStatus = CallStatus.Trying,
this.callId = req.get('Call-ID'); this.callId = req.get('Call-ID');
this.sipStatus = 100; this.sipStatus = 100;
this.sipReason = 'Trying';
this.from = from || req.callingNumber; this.from = from || req.callingNumber;
this.to = to; this.to = to;
if (tag) this._customerData = tag; if (tag) this._customerData = tag;
@@ -81,9 +84,10 @@ class CallInfo {
* @param {string} callStatus - current call status * @param {string} callStatus - current call status
* @param {number} sipStatus - current sip status * @param {number} sipStatus - current sip status
*/ */
updateCallStatus(callStatus, sipStatus) { updateCallStatus(callStatus, sipStatus, sipReason) {
this.callStatus = callStatus; this.callStatus = callStatus;
if (sipStatus) this.sipStatus = sipStatus; if (sipStatus) this.sipStatus = sipStatus;
if (sipReason) this.sipReason = sipReason;
} }
/** /**
@@ -106,6 +110,7 @@ class CallInfo {
to: this.to, to: this.to,
callId: this.callId, callId: this.callId,
sipStatus: this.sipStatus, sipStatus: this.sipStatus,
sipReason: this.sipReason,
callStatus: this.callStatus, callStatus: this.callStatus,
callerId: this.callerId, callerId: this.callerId,
accountSid: this.accountSid, accountSid: this.accountSid,

View File

@@ -521,6 +521,29 @@ class CallSession extends Emitter {
task.doConferenceMuteNonModerators(this, opts); task.doConferenceMuteNonModerators(this, opts);
} }
async _lccSipRequest(callSid, opts) {
const {sip_request} = opts;
const {method, content_type, content, headers = {}} = sip_request;
if (!this.hasStableDialog) {
this.logger.info('CallSession:_lccSipRequest - invalid command as we do not have a stable call');
return;
}
try {
const res = await this.dlg.request({
method,
headers: {
...headers,
'Content-Type': content_type,
'Content': content
}
});
this.logger.debug({res}, `CallSession:_lccSipRequest got response to ${method}`);
return res;
} catch (err) {
this.logger.error({err}, `CallSession:_lccSipRequest - error sending ${method}`);
}
}
/** /**
* perform live call control -- whisper to one party or the other on a call * perform live call control -- whisper to one party or the other on a call
* @param {array} opts - array of play or say tasks * @param {array} opts - array of play or say tasks
@@ -596,6 +619,10 @@ class CallSession extends Emitter {
else if (opts.conf_mute_status) { else if (opts.conf_mute_status) {
await this._lccConfMuteStatus(callSid, opts); await this._lccConfMuteStatus(callSid, opts);
} }
else if (opts.sip_request) {
const res = await this._lccSipRequest(callSid, opts);
return {status: res.status, reason: res.reason};
}
// whisper may be the only thing we are asked to do, or it may that // 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.. // we are doing a whisper after having muted, paused reccording etc..
@@ -752,7 +779,11 @@ class CallSession extends Emitter {
} catch (err) { } catch (err) {
if (err === CALLER_CANCELLED_ERR_MSG) { if (err === CALLER_CANCELLED_ERR_MSG) {
this.logger.error(err, 'caller canceled quickly before we could respond, ending call'); this.logger.error(err, 'caller canceled quickly before we could respond, ending call');
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487}); this._notifyCallStatusChange({
callStatus: CallStatus.NoAnswer,
sipStatus: 487,
sipReason: 'Request Terminated'
});
this._callReleased(); this._callReleased();
} }
else { else {
@@ -862,9 +893,10 @@ class CallSession extends Emitter {
this.dlg.on('destroy', this._callerHungup.bind(this)); this.dlg.on('destroy', this._callerHungup.bind(this));
this.wrapDialog(this.dlg); this.wrapDialog(this.dlg);
this.dlg.callSid = this.callSid; this.dlg.callSid = this.callSid;
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress}); this.emit('callStatusChange', {sipStatus: 200, sipReason: 'OK', callStatus: CallStatus.InProgress});
this.dlg.on('modify', this._onReinvite.bind(this)); this.dlg.on('modify', this._onReinvite.bind(this));
this.dlg.on('refer', this._onRefer.bind(this));
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`); this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
} }
@@ -890,6 +922,22 @@ class CallSession extends Emitter {
} }
} }
/**
* Handle incoming REFER if we are in a dial task
* @param {*} req
* @param {*} res
*/
_onRefer(req, res) {
const task = this.currentTask;
const sd = task.sd;
if (task && TaskName.Dial === task.name && sd) {
task.handleRefer(this, req, res);
}
else {
res.send(501);
}
}
/** /**
* create and endpoint if we don't have one; otherwise simply return * create and endpoint if we don't have one; otherwise simply return
* the current media server and endpoint that are associated with this call * the current media server and endpoint that are associated with this call
@@ -1075,7 +1123,7 @@ class CallSession extends Emitter {
* @param {number} sipStatus - current sip status * @param {number} sipStatus - current sip status
* @param {number} [duration] - duration of a completed call, in seconds * @param {number} [duration] - duration of a completed call, in seconds
*/ */
_notifyCallStatusChange({callStatus, sipStatus, duration}) { _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
if (this.callMoved) return; if (this.callMoved) return;
/* race condition: we hang up at the same time as the caller */ /* race condition: we hang up at the same time as the caller */
@@ -1088,7 +1136,7 @@ class CallSession extends Emitter {
(!duration && callStatus !== CallStatus.Completed), (!duration && callStatus !== CallStatus.Completed),
'duration MUST be supplied when call completed AND ONLY when call completed'); 'duration MUST be supplied when call completed AND ONLY when call completed');
this.callInfo.updateCallStatus(callStatus, sipStatus); this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
if (typeof duration === 'number') this.callInfo.duration = duration; if (typeof duration === 'number') this.callInfo.duration = duration;
try { try {
this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON()); this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON());

View File

@@ -30,6 +30,10 @@ class ConfirmCallSession extends CallSession {
_clearResources() { _clearResources() {
} }
_callerHungup() {
}
} }
module.exports = ConfirmCallSession; module.exports = ConfirmCallSession;

View File

@@ -24,11 +24,19 @@ class InboundCallSession extends CallSession {
req.once('cancel', this._onCancel.bind(this)); req.once('cancel', this._onCancel.bind(this));
this.on('callStatusChange', this._notifyCallStatusChange.bind(this)); this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100}); this._notifyCallStatusChange({
callStatus: CallStatus.Trying,
sipStatus: 100,
sipReason: 'Trying'
});
} }
_onCancel() { _onCancel() {
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487}); this._notifyCallStatusChange({
callStatus: CallStatus.NoAnswer,
sipStatus: 487,
sipReason: 'Request Terminated'
});
this._callReleased(); this._callReleased();
} }
@@ -56,7 +64,10 @@ class InboundCallSession extends CallSession {
_callerHungup() { _callerHungup() {
assert(this.dlg.connectTime); assert(this.dlg.connectTime);
const duration = moment().diff(this.dlg.connectTime, 'seconds'); const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); this.emit('callStatusChange', {
callStatus: CallStatus.Completed,
duration
});
this.logger.debug('InboundCallSession: caller hung up'); this.logger.debug('InboundCallSession: caller hung up');
this._callReleased(); this._callReleased();
this.req.removeAllListeners('cancel'); this.req.removeAllListeners('cancel');

View File

@@ -22,7 +22,11 @@ class RestCallSession extends CallSession {
this.ep = ep; this.ep = ep;
this.on('callStatusChange', this._notifyCallStatusChange.bind(this)); this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100}); this._notifyCallStatusChange({
callStatus: CallStatus.Trying,
sipStatus: 100,
sipReason: 'Trying'
});
} }
/** /**

View File

@@ -14,6 +14,7 @@ const sessionTracker = require('../session/session-tracker');
const DtmfCollector = require('../utils/dtmf-collector'); const DtmfCollector = require('../utils/dtmf-collector');
const dbUtils = require('../utils/db-utils'); const dbUtils = require('../utils/db-utils');
const debug = require('debug')('jambonz:feature-server'); const debug = require('debug')('jambonz:feature-server');
const {parseUri} = require('drachtio-srf');
function parseDtmfOptions(logger, dtmfCapture) { function parseDtmfOptions(logger, dtmfCapture) {
let parentDtmfCollector, childDtmfCollector; let parentDtmfCollector, childDtmfCollector;
@@ -91,6 +92,7 @@ class TaskDial extends Task {
this.timeLimit = this.data.timeLimit; this.timeLimit = this.data.timeLimit;
this.confirmHook = this.data.confirmHook; this.confirmHook = this.data.confirmHook;
this.confirmMethod = this.data.confirmMethod; this.confirmMethod = this.data.confirmMethod;
this.referHook = this.data.referHook;
this.dtmfHook = this.data.dtmfHook; this.dtmfHook = this.data.dtmfHook;
this.proxy = this.data.proxy; this.proxy = this.data.proxy;
@@ -245,6 +247,40 @@ class TaskDial extends Task {
} }
} }
async handleRefer(cs, req, res, callInfo = cs.callInfo) {
if (this.referHook) {
try {
const isChild = !!callInfo.parentCallSid;
const referring_call_sid = isChild ? callInfo.callSid : cs.callSid;
const referred_call_sid = isChild ? callInfo.parentCallSid : this.sd.callSid;
const to = parseUri(req.getParsedHeader('Refer-To').uri);
const by = parseUri(req.getParsedHeader('Referred-By').uri);
this.logger.info({to}, 'refer to parsed');
await cs.requestor.request('verb:hook', this.referHook, {
...callInfo,
refer_details: {
sip_refer_to: req.get('Refer-To'),
sip_referred_by: req.get('Referred-By'),
sip_user_agent: req.get('User-Agent'),
refer_to_user: to.user,
referred_by_user: by.user,
referring_call_sid,
referred_call_sid
}
});
res.send(202);
this.logger.info('DialTask:handleRefer - sent 202 Accepted');
} catch (err) {
res.send(err.statusCode || 501);
}
}
else {
this.logger.info('DialTask:handleRefer - got REFER but no referHook, responding 501');
res.send(501);
}
}
_removeHandlers(sd) { _removeHandlers(sd) {
sd.removeAllListeners('accept'); sd.removeAllListeners('accept');
sd.removeAllListeners('decline'); sd.removeAllListeners('decline');
@@ -389,6 +425,7 @@ class TaskDial extends Task {
this.dials.set(sd.callSid, sd); this.dials.set(sd.callSid, sd);
sd sd
.on('refer', (callInfo, req, res) => this.handleRefer(cs, req, res, callInfo))
.on('callCreateFail', () => { .on('callCreateFail', () => {
clearTimeout(this.timerRing); clearTimeout(this.timerRing);
this.dials.delete(sd.callSid); this.dials.delete(sd.callSid);
@@ -457,6 +494,9 @@ class TaskDial extends Task {
} catch (err) { } catch (err) {
this.logger.error(err, 'Error in dial einvite from B leg'); this.logger.error(err, 'Error in dial einvite from B leg');
} }
})
.on('refer', (callInfo, req, res) => {
}) })
.once('adulting', () => { .once('adulting', () => {
/* child call just adulted and got its own session */ /* child call just adulted and got its own session */

View File

@@ -26,7 +26,7 @@ class TaskGather extends Task {
if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true; if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true;
this.timeout = (this.timeout || 15) * 1000; this.timeout = (this.timeout || 15) * 1000;
this.interim = this.partialResultCallback; this.interim = this.partialResultHook || this.bargein;
if (this.data.recognizer) { if (this.data.recognizer) {
const recognizer = this.data.recognizer; const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor; this.vendor = recognizer.vendor;
@@ -209,7 +209,7 @@ class TaskGather extends Task {
if (this.hints && this.hints.length > 1) { if (this.hints && this.hints.length > 1) {
opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(','); opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
} }
if (this.altLanguages && this.altLanguages.length > 1) { if (this.altLanguages && this.altLanguages.length > 0) {
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(','); opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
} }
if (this.profanityFilter === true) { if (this.profanityFilter === true) {
@@ -259,7 +259,7 @@ class TaskGather extends Task {
ep.startTranscription({ ep.startTranscription({
vendor: this.vendor, vendor: this.vendor,
locale: this.language, locale: this.language,
interim: this.partialResultCallback || this.bargein, interim: this.interim,
}).catch((err) => { }).catch((err) => {
const {writeAlerts, AlertType} = this.cs.srf.locals; const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error'); this.logger.error(err, 'TaskGather:_startTranscribing error');

View File

@@ -19,7 +19,11 @@ class TaskSipDecline extends Task {
res.send(this.data.status, this.data.reason, { res.send(this.data.status, this.data.reason, {
headers: this.headers headers: this.headers
}); });
cs.emit('callStatusChange', {callStatus: CallStatus.Failed, sipStatus: this.data.status}); cs.emit('callStatusChange', {
callStatus: CallStatus.Failed,
sipStatus: this.data.status,
sipReason: this.data.reason
});
} }
} }

View File

@@ -140,6 +140,7 @@
"answerOnBridge": "boolean", "answerOnBridge": "boolean",
"callerId": "string", "callerId": "string",
"confirmHook": "object|string", "confirmHook": "object|string",
"referHook": "object|string",
"dialMusic": "string", "dialMusic": "string",
"dtmfCapture": "object", "dtmfCapture": "object",
"dtmfHook": "object|string", "dtmfHook": "object|string",

View File

@@ -70,8 +70,14 @@ class HttpRequestor extends BaseRequestor {
await this.post(url, payload, headers) : await this.post(url, payload, headers) :
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers); await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
} catch (err) { } catch (err) {
this.logger.error({err, secret: this.secret, baseUrl: this.baseUrl, url, statusCode: err.statusCode}, if (err.statusCode) {
`web callback returned unexpected error code ${err.statusCode}`); this.logger.info({baseUrl: this.baseUrl, url},
`web callback returned unexpected status code ${err.statusCode}`);
}
else {
this.logger.error({err, baseUrl: this.baseUrl, url},
'web callback returned unexpected error');
}
let opts = {account_sid: this.account_sid}; let opts = {account_sid: this.account_sid};
if (err.code === 'ECONNREFUSED') { if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url}; opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};

View File

@@ -164,10 +164,14 @@ class SingleDialer extends Emitter {
callId: this.callInfo.callId callId: this.callInfo.callId
}); });
this.inviteInProgress = req; this.inviteInProgress = req;
this.emit('callStatusChange', {callStatus: CallStatus.Trying, sipStatus: 100}); this.emit('callStatusChange', {
callStatus: CallStatus.Trying,
sipStatus: 100,
sipReason: 'Trying'
});
}, },
cbProvisional: (prov) => { cbProvisional: (prov) => {
const status = {sipStatus: prov.status}; const status = {sipStatus: prov.status, sipReason: prov.reason};
if ([180, 183].includes(prov.status) && prov.body) { if ([180, 183].includes(prov.status) && prov.body) {
if (status.callStatus !== CallStatus.EarlyMedia) { if (status.callStatus !== CallStatus.EarlyMedia) {
status.callStatus = CallStatus.EarlyMedia; status.callStatus = CallStatus.EarlyMedia;
@@ -182,7 +186,11 @@ class SingleDialer extends Emitter {
await connectStream(this.dlg.remote.sdp); await connectStream(this.dlg.remote.sdp);
this.dlg.callSid = this.callSid; this.dlg.callSid = this.callSid;
this.inviteInProgress = null; this.inviteInProgress = null;
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress}); this.emit('callStatusChange', {
sipStatus: 200,
sipReason: 'OK',
callStatus: CallStatus.InProgress
});
this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`); this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`);
const connectTime = this.dlg.connectTime = moment(); const connectTime = this.dlg.connectTime = moment();
@@ -190,7 +198,12 @@ class SingleDialer extends Emitter {
if (this.killed) { if (this.killed) {
this.logger.info(`SingleDialer:exec race condition - we were killed as call connected: ${this.callSid}`); this.logger.info(`SingleDialer:exec race condition - we were killed as call connected: ${this.callSid}`);
const duration = moment().diff(connectTime, 'seconds'); const duration = moment().diff(connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); this.emit('callStatusChange', {
callStatus: CallStatus.Completed,
sipStatus: 487,
sipReason: 'Request Terminated',
duration
});
if (this.ep) this.ep.destroy(); if (this.ep) this.ep.destroy();
return; return;
} }
@@ -217,6 +230,9 @@ class SingleDialer extends Emitter {
} catch (err) { } catch (err) {
this.logger.error(err, 'Error handling reinvite'); this.logger.error(err, 'Error handling reinvite');
} }
})
.on('refer', (req, res) => {
this.emit('refer', this.callInfo, req, res);
}); });
if (this.confirmHook) this._executeApp(this.confirmHook); if (this.confirmHook) this._executeApp(this.confirmHook);
@@ -226,6 +242,7 @@ class SingleDialer extends Emitter {
const status = {callStatus: CallStatus.Failed}; const status = {callStatus: CallStatus.Failed};
if (err instanceof SipError) { if (err instanceof SipError) {
status.sipStatus = err.status; status.sipStatus = err.status;
status.sipReason = err.reason;
if (err.status === 487) status.callStatus = CallStatus.NoAnswer; if (err.status === 487) status.callStatus = CallStatus.NoAnswer;
else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy; else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy;
this.logger.info(`SingleDialer:exec outdial failure ${err.status}`); this.logger.info(`SingleDialer:exec outdial failure ${err.status}`);
@@ -348,13 +365,13 @@ class SingleDialer extends Emitter {
}); });
} }
_notifyCallStatusChange({callStatus, sipStatus, duration}) { _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) || assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
(!duration && callStatus !== CallStatus.Completed), (!duration && callStatus !== CallStatus.Completed),
'duration MUST be supplied when call completed AND ONLY when call completed'); 'duration MUST be supplied when call completed AND ONLY when call completed');
if (this.callInfo) { if (this.callInfo) {
this.callInfo.updateCallStatus(callStatus, sipStatus); this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
if (typeof duration === 'number') this.callInfo.duration = duration; if (typeof duration === 'number') this.callInfo.duration = duration;
try { try {
this.requestor.request('call:status', this.application.call_status_hook, this.callInfo.toJSON()); this.requestor.request('call:status', this.application.call_status_hook, this.callInfo.toJSON());