mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 08:40:38 +00:00
work on say and gather
This commit is contained in:
@@ -1,142 +1,356 @@
|
||||
const Task = require('./task');
|
||||
const name = 'dial';
|
||||
const makeTask = require('./make_task');
|
||||
const {CallStatus, CallDirection, TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const SipError = require('drachtio-srf').SipError;
|
||||
const assert = require('assert');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const request = require('request');
|
||||
const moment = require('moment');
|
||||
|
||||
function isFinalCallStatus(status) {
|
||||
return [CallStatus.Completed, CallStatus.NoAnswer, CallStatus.Failed, CallStatus.Busy].includes(status);
|
||||
}
|
||||
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 = {};
|
||||
this.preconditions = TaskPreconditions.None;
|
||||
|
||||
this.action = opts.action;
|
||||
this.earlyMedia = opts.answerOnBridge === true;
|
||||
this.callerId = opts.callerId;
|
||||
this.dialMusic = opts.dialMusic;
|
||||
this.headers = this.data.headers || {};
|
||||
this.method = opts.method || 'POST';
|
||||
this.statusCallback = opts.statusCallback;
|
||||
this.statusCallbackMethod = opts.statusCallbackMethod || 'POST';
|
||||
this.target = opts.target;
|
||||
this.timeout = opts.timeout || 60;
|
||||
this.timeLimit = opts.timeLimit;
|
||||
|
||||
if (opts.transcribe) {
|
||||
this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe});
|
||||
}
|
||||
if (opts.listen) {
|
||||
this.listenTask = makeTask(logger, {'listen': opts.transcribe});
|
||||
}
|
||||
if (opts.transcribe) {
|
||||
this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe});
|
||||
}
|
||||
|
||||
this.canceled = false;
|
||||
this.callAttributes = {};
|
||||
this.dialCallStatus = CallStatus.Failed;
|
||||
this.dialCallSid = null;
|
||||
this.dialCallDuration = null;
|
||||
|
||||
this.on('callStatusChange', this._onCallStatusChange.bind(this));
|
||||
}
|
||||
|
||||
static get name() { return name; }
|
||||
get name() { return TaskName.Dial; }
|
||||
|
||||
/**
|
||||
* Reject an incoming call attempt with a provided status code and (optionally) reason
|
||||
*/
|
||||
async exec(cs) {
|
||||
try {
|
||||
this._initializeCallData(cs);
|
||||
await this._initializeInbound(cs);
|
||||
//await connectCall(cs);
|
||||
await this._untilCallEnds(cs);
|
||||
await this._attemptCalls(cs);
|
||||
await this._waitForCompletion(cs);
|
||||
} catch (err) {
|
||||
this.logger.info(`TaskDial:exec terminating with error ${err.message}`);
|
||||
this.logger.error(`TaskDial:exec terminating with error ${err.message}`);
|
||||
}
|
||||
await this._actionHook(cs);
|
||||
this.clearResources();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_initializeCallData(cs) {
|
||||
this.logger.debug(`TaskDial:_initializeCallData parent call sid is ${cs.callSid}`);
|
||||
Object.assign(this.callAttributes, {
|
||||
AccountSid: cs.AccountSid,
|
||||
ParentCallSid: cs.callSid,
|
||||
Direction: CallDirection.Outbound
|
||||
});
|
||||
}
|
||||
|
||||
async _initializeInbound(cs) {
|
||||
const {req, res} = cs;
|
||||
const {req} = 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'});
|
||||
}
|
||||
const result = await cs.connectInboundCallToIvr(this.earlyMedia);
|
||||
if (!result) throw new Error('outbound dial via API not supported yet');
|
||||
|
||||
const {ep, dlg, res} = result;
|
||||
assert(ep);
|
||||
// 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}`);
|
||||
});
|
||||
}
|
||||
this.epIn = ep;
|
||||
this.dlgIn = dlg;
|
||||
this.res = res;
|
||||
} 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();
|
||||
async _attemptCalls(cs) {
|
||||
const {req, srf} = cs;
|
||||
|
||||
// send all outbound calls back to originating SBC for simplicity
|
||||
const sbcAddress = `${req.source_address}:${req.source_port}`;
|
||||
|
||||
const callSid = uuidv4();
|
||||
let newCallId, to, from;
|
||||
try {
|
||||
// create an endpoint for the outbound call
|
||||
const epOut = await cs.createEndpoint();
|
||||
this.addResource('epOut', epOut);
|
||||
|
||||
const {uri, opts} = this._prepareOutdialAttempt(this.target[0], sbcAddress,
|
||||
this.callerId || req.callingNumber, epOut.local.sdp);
|
||||
|
||||
let streamConnected = false;
|
||||
|
||||
const connectStreams = async(remoteSdp) => {
|
||||
streamConnected = true;
|
||||
epOut.modify(remoteSdp);
|
||||
this.epIn.bridge(epOut);
|
||||
if (!this.dlgIn) {
|
||||
this.dlgIn = await cs.srf.answerParentCall(this.epIn.local.sdp);
|
||||
}
|
||||
};
|
||||
|
||||
// outdial requested destination
|
||||
const uac = await srf.createUAC(uri, opts, {
|
||||
cbRequest: (err, reqSent) => {
|
||||
this.outboundInviteInProgress = reqSent;
|
||||
newCallId = req.get('Call-ID');
|
||||
from = reqSent.callingNumber,
|
||||
to = reqSent.calledNumber;
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
CallStatus: CallStatus.Trying,
|
||||
From: from,
|
||||
To: to,
|
||||
SipStatus: 100
|
||||
});
|
||||
},
|
||||
cbProvisional: (prov) => {
|
||||
if ([180, 183].includes(prov.status)) {
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
CallStatus: prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing,
|
||||
From: from,
|
||||
To: to,
|
||||
SipStatus: prov.status
|
||||
});
|
||||
if (!streamConnected && prov.body) connectStreams(prov.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// outbound call was established
|
||||
uac.connectTime = moment();
|
||||
uac.callSid = this.dialCallSid = callSid;
|
||||
uac.from = from;
|
||||
uac.to = to;
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
From: from,
|
||||
To: to,
|
||||
CallStatus: CallStatus.InProgress,
|
||||
SipStatus: 200
|
||||
});
|
||||
uac.on('destroy', () => {
|
||||
const duration = this.dialCallDuration = moment().diff(uac.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
From: from,
|
||||
To: to,
|
||||
CallStatus: CallStatus.Completed,
|
||||
Duration: duration
|
||||
});
|
||||
});
|
||||
if (!streamConnected) connectStreams(uac.remote.sdp);
|
||||
this.outboundInviteInProgress = null;
|
||||
this.addResource('dlgOut', uac);
|
||||
} catch (err) {
|
||||
if (err instanceof SipError) {
|
||||
switch (err.status) {
|
||||
case 487:
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
From: from,
|
||||
To: to,
|
||||
CallStatus: CallStatus.NoAnswer,
|
||||
SipStatus: err.status
|
||||
});
|
||||
break;
|
||||
case 486:
|
||||
case 600:
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
From: from,
|
||||
To: to,
|
||||
CallStatus: CallStatus.Busy,
|
||||
SipStatus: err.status
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.emit('callStatusChange', {callSid,
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
From: from,
|
||||
To: to,
|
||||
CallStatus: CallStatus.Failed,
|
||||
SipStatus: err.status
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (err.status !== 487) {
|
||||
this.logger.info(`TaskDial:_connectCall outdial failed with ${err.status}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: callSid,
|
||||
SipCallId: newCallId,
|
||||
From: from,
|
||||
To: to,
|
||||
CallStatus: CallStatus.Failed,
|
||||
SipStatus: 500
|
||||
});
|
||||
this.logger.error(err, 'TaskDial:_connectCall error');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.localResources = {};
|
||||
}
|
||||
|
||||
_prepareOutdialAttempt(target, sbcAddress, callerId, sdp) {
|
||||
const opts = {
|
||||
headers: this.headers,
|
||||
proxy: `sip:${sbcAddress}`,
|
||||
callingNumber: callerId,
|
||||
localSdp: sdp
|
||||
};
|
||||
let uri;
|
||||
|
||||
switch (target.type) {
|
||||
case 'phone':
|
||||
uri = `sip:${target.number}@${sbcAddress}`;
|
||||
break;
|
||||
case 'sip':
|
||||
uri = target.uri;
|
||||
if (target.auth) Object.assign(opts, {auth: target.auth});
|
||||
break;
|
||||
case 'user':
|
||||
uri = `sip:${target.name}`;
|
||||
break;
|
||||
default:
|
||||
assert(0, `TaskDial:_prepareOutdialAttempt invalid target type ${target.type}; please fix specs.json`);
|
||||
}
|
||||
return {uri, opts};
|
||||
}
|
||||
|
||||
_onCancel(cs) {
|
||||
this.logger.info('TaskDial: caller hung up before connecting');
|
||||
this.canceled = this.finished = true;
|
||||
this._clearResources();
|
||||
cs.emit('callStatusChange', {status: 'canceled'});
|
||||
this.canceled = true;
|
||||
cs.emit('callStatusChange', {status: CallStatus.NoAnswer});
|
||||
}
|
||||
|
||||
_onCallerHangup(cs, dlg) {
|
||||
cs.emit('callStatusChange', {status: 'canceled'});
|
||||
this.finished = true;
|
||||
this._clearResources();
|
||||
this.logger.info('TaskDial: caller hung up');
|
||||
cs.emit('callStatusChange', {status: CallStatus.Completed});
|
||||
if (this.outboundInviteInProgress) this.outboundInviteInProgress.cancel();
|
||||
|
||||
// we are going to hang up the B leg shortly..so
|
||||
const dlgOut = this.getResource('dlgOut');
|
||||
if (dlgOut) {
|
||||
const duration = this.dialCallDuration = moment().diff(dlgOut.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {
|
||||
CallSid: dlgOut.callSid,
|
||||
SipCallId: dlgOut.sip.callId,
|
||||
From: dlgOut.from,
|
||||
To: dlgOut.to,
|
||||
CallStatus: CallStatus.Completed,
|
||||
Duration: duration
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns a Promise that resolves when the call ends
|
||||
* returns a Promise that resolves when either party hangs up
|
||||
*/
|
||||
_untilCallEnds(cs) {
|
||||
const {res} = cs;
|
||||
_waitForCompletion(cs) {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
assert(!this.finished);
|
||||
const dlgOut = this.getResource('dlgOut');
|
||||
assert(this.dlgIn && dlgOut);
|
||||
assert(this.dlgIn.connected && dlgOut.connected);
|
||||
|
||||
//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());
|
||||
});
|
||||
*/
|
||||
[this.dlgIn, dlgOut].forEach((dlg) => dlg.on('destroy', () => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
_onCallStatusChange(evt) {
|
||||
this.logger.debug(evt, 'TaskDial:_onCallStatusChange');
|
||||
|
||||
// save the most recent final call status of a B leg, until we get one that is completed
|
||||
if (isFinalCallStatus(evt.CallStatus) && this.dialCallStatus !== CallStatus.Completed) {
|
||||
this.dialCallStatus = evt.CallStatus;
|
||||
}
|
||||
if (this.statusCallback) {
|
||||
const params = Object.assign({}, this.callAttributes, evt);
|
||||
const opts = {
|
||||
url: this.statusCallback,
|
||||
method: this.statusCallbackMethod,
|
||||
json: true,
|
||||
qs: 'GET' === this.statusCallbackMethod ? params : null,
|
||||
body: 'POST' === this.statusCallbackMethod ? params : null
|
||||
};
|
||||
request(opts, (err) => {
|
||||
if (err) this.logger.info(`TaskDial:Error sending call status to ${this.statusCallback}: ${err.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _actionHook(cs) {
|
||||
if (this.action) {
|
||||
const params = {DialCallStatus: this.dialCallStatus};
|
||||
Object.assign(params, {
|
||||
DialCallSid: this.dialCallSid,
|
||||
DialCallDuration: this.dialCallDuration
|
||||
});
|
||||
const opts = {
|
||||
url: this.action,
|
||||
method: this.method,
|
||||
json: true,
|
||||
qs: 'GET' === this.method ? params : null,
|
||||
body: 'POST' === this.method ? params : null
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(opts, (err, response, body) => {
|
||||
if (err) this.logger.info(`TaskDial:_actionHook sending call status to ${this.action}: ${err.message}`);
|
||||
if (body) {
|
||||
this.logger.debug(body, 'got new application payload');
|
||||
cs.replaceApplication(body);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskDial;
|
||||
|
||||
Reference in New Issue
Block a user