mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
217 lines
7.6 KiB
JavaScript
217 lines
7.6 KiB
JavaScript
const Emitter = require('events');
|
|
const {CallStatus} = require('./constants');
|
|
const uuidv4 = require('uuid/v4');
|
|
const SipError = require('drachtio-srf').SipError;
|
|
const {TaskPreconditions} = require('../utils/constants');
|
|
const assert = require('assert');
|
|
const ConfirmCallSession = require('../session/confirm-call-session');
|
|
const hooks = require('./notifiers');
|
|
const moment = require('moment');
|
|
|
|
class SingleDialer extends Emitter {
|
|
constructor({logger, sbcAddress, target, opts, application, callInfo}) {
|
|
super();
|
|
assert(target.type);
|
|
|
|
this.logger = logger;
|
|
this.target = target;
|
|
this.sbcAddress = sbcAddress;
|
|
this.opts = opts;
|
|
this.application = application;
|
|
this.url = opts.url;
|
|
this.method = opts.method;
|
|
|
|
this._callSid = uuidv4();
|
|
this.bindings = logger.bindings();
|
|
this.callInfo = Object.assign({}, callInfo, {callSid: this._callSid});
|
|
this.sipStatus;
|
|
this.callGone = false;
|
|
|
|
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
|
}
|
|
|
|
get callSid() {
|
|
return this._callSid;
|
|
}
|
|
get callStatus() {
|
|
return this.callInfo.callStatus;
|
|
}
|
|
|
|
async exec(srf, ms, opts) {
|
|
let uri, to;
|
|
switch (this.target.type) {
|
|
case 'phone':
|
|
assert(this.target.number);
|
|
uri = `sip:${this.target.number}@${this.sbcAddress}`;
|
|
to = this.target.number;
|
|
break;
|
|
case 'user':
|
|
assert(this.target.name);
|
|
uri = `sip:${this.target.name}`;
|
|
to = this.target.name;
|
|
break;
|
|
case 'sip':
|
|
assert(this.target.sipUri);
|
|
uri = this.target.sipUri;
|
|
to = this.target.sipUri;
|
|
break;
|
|
default:
|
|
// should have been caught by parser
|
|
assert(false, `invalid dial type ${this.target.type}: must be phone, user, or sip`);
|
|
}
|
|
|
|
try {
|
|
this.ep = await ms.createEndpoint();
|
|
this.logger.debug(`SingleDialer:exec - created endpoint ${this.ep.uuid}`);
|
|
let sdp;
|
|
const connectStream = async(remoteSdp) => {
|
|
if (remoteSdp !== sdp) {
|
|
this.ep.modify(sdp = remoteSdp);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
Object.assign(opts, {
|
|
proxy: `sip:${this.sbcAddress}`,
|
|
localSdp: this.ep.local.sdp
|
|
});
|
|
if (this.target.auth) opts.auth = this.target.auth;
|
|
this.dlg = await srf.createUAC(uri, opts, {
|
|
cbRequest: (err, req) => {
|
|
if (err) return this.logger.error(err, 'SingleDialer:exec Error creating call');
|
|
|
|
/**
|
|
* INVITE has been sent out
|
|
* (a) create a logger for this call
|
|
* (b) augment this.callInfo with additional call info
|
|
*/
|
|
this.logger = srf.locals.parentLogger.child({
|
|
callSid: this.callSid,
|
|
parentCallSid: this.bindings.callSid,
|
|
callId: req.get('Call-ID')
|
|
});
|
|
this.inviteInProgress = req;
|
|
const status = {callStatus: CallStatus.Trying, sipStatus: 100};
|
|
Object.assign(this.callInfo, {callId: req.get('Call-ID'), from: req.callingNumber, to});
|
|
const {actionHook, notifyHook} = hooks(this.logger, this.callInfo);
|
|
this.actionHook = actionHook;
|
|
this.notifyHook = notifyHook;
|
|
this.emit('callStatusChange', status);
|
|
},
|
|
cbProvisional: (prov) => {
|
|
const status = {sipStatus: prov.status};
|
|
if ([180, 183].includes(prov.status) && prov.body) {
|
|
status.callStatus = CallStatus.EarlyMedia;
|
|
if (connectStream(prov.body)) this.emit('earlyMedia');
|
|
}
|
|
else status.callStatus = CallStatus.Ringing;
|
|
this.emit('callStatusChange', status);
|
|
}
|
|
});
|
|
connectStream(this.dlg.remote.sdp);
|
|
this.dlg.callSid = this.callSid;
|
|
this.inviteInProgress = null;
|
|
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
|
this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`);
|
|
const connectTime = this.dlg.connectTime = moment();
|
|
|
|
this.dlg.on('destroy', () => {
|
|
const duration = moment().diff(connectTime, 'seconds');
|
|
this.logger.debug('SingleDialer:exec called party hung up');
|
|
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
|
this.ep.destroy();
|
|
});
|
|
|
|
if (this.url) this._executeApp(this.url);
|
|
else this.emit('accept');
|
|
} catch (err) {
|
|
const status = {callStatus: CallStatus.Failed};
|
|
if (err instanceof SipError) {
|
|
status.sipStatus = err.status;
|
|
if (err.status === 487) status.callStatus = CallStatus.NoAnswer;
|
|
else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy;
|
|
this.logger.debug(`SingleDialer:exec outdial failure ${err.status}`);
|
|
}
|
|
else {
|
|
this.logger.error(err, 'SingleDialer:exec');
|
|
status.sipStatus = 500;
|
|
}
|
|
this.emit('callStatusChange', status);
|
|
if (this.ep) this.ep.destroy();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* kill the call in progress or the stable dialog, whichever we have
|
|
*/
|
|
async kill() {
|
|
if (this.inviteInProgress) await this.inviteInProgress.cancel();
|
|
else if (this.dlg && this.dlg.connected) {
|
|
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
|
this.logger.debug('SingleDialer:kill hanging up called party');
|
|
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
|
}
|
|
if (this.ep) {
|
|
this.logger.debug(`SingleDialer:kill - deleting endpoint ${this.ep.uuid}`);
|
|
await this.ep.destroy();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run an application on the call after answer, e.g. call screening.
|
|
* Once the application completes in some fashion, emit an 'accepted' event
|
|
* if the call is still up/connected, a 'decline' otherwise.
|
|
* 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`);
|
|
try {
|
|
const tasks = await this.actionHook(this.url, this.method);
|
|
const allowedTasks = tasks.filter((task) => {
|
|
return [
|
|
TaskPreconditions.StableCall,
|
|
TaskPreconditions.Endpoint
|
|
].includes(task.preconditions);
|
|
});
|
|
if (tasks.length !== allowedTasks.length) {
|
|
throw new Error('unsupported verb in dial url');
|
|
}
|
|
|
|
this.logger.debug(`SingleDialer:_executeApp: executing ${tasks.length} tasks`);
|
|
const cs = new ConfirmCallSession(this.logger, this.application, this.dlg, this.ep, tasks);
|
|
await cs.exec();
|
|
this.emit(this.dlg.connected ? 'accept' : 'decline');
|
|
} catch (err) {
|
|
this.logger.debug(err, 'SingleDialer:_executeApp: error');
|
|
this.emit('decline');
|
|
if (this.dlg.connected) this.dlg.destroy();
|
|
}
|
|
}
|
|
|
|
_notifyCallStatusChange(callStatus) {
|
|
try {
|
|
const auth = {};
|
|
if (this.application.hook_basic_auth_user && this.application.hook_basic_auth_password) {
|
|
Object.assign(auth, {user: this.application.hook_basic_auth_user, password: this.hook_basic_auth_password});
|
|
}
|
|
this.notifyHook(this.application.call_status_hook,
|
|
this.application.hook_http_method,
|
|
auth,
|
|
callStatus);
|
|
} catch (err) {
|
|
this.logger.info(err, `SingleDialer:_notifyCallStatusChange: error sending ${JSON.stringify(callStatus)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo}) {
|
|
const sd = new SingleDialer({logger, sbcAddress, target, opts, application, callInfo});
|
|
sd.exec(srf, ms, opts);
|
|
return sd;
|
|
}
|
|
|
|
module.exports = placeOutdial;
|
|
|