mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 08:40:38 +00:00
229 lines
7.3 KiB
JavaScript
229 lines
7.3 KiB
JavaScript
const Task = require('./task');
|
|
const makeTask = require('./make_task');
|
|
const {CallStatus, CallDirection, TaskName, TaskPreconditions} = require('../utils/constants');
|
|
const assert = require('assert');
|
|
const placeCall = require('../utils/place-outdial');
|
|
const config = require('config');
|
|
const moment = require('moment');
|
|
const debug = require('debug')('jambonz:feature-server');
|
|
|
|
class TaskDial extends Task {
|
|
constructor(logger, opts) {
|
|
super(logger, opts);
|
|
this.preconditions = TaskPreconditions.None;
|
|
|
|
this.earlyMedia = this.data.answerOnBridge === true;
|
|
this.callerId = this.data.callerId;
|
|
this.dialMusic = this.data.dialMusic;
|
|
this.headers = this.data.headers || {};
|
|
this.method = this.data.method || 'POST';
|
|
this.statusCallback = this.data.statusCallback;
|
|
this.statusCallbackMethod = this.data.statusCallbackMethod || 'POST';
|
|
this.target = this.data.target;
|
|
this.timeout = this.data.timeout || 60;
|
|
this.timeLimit = this.data.timeLimit;
|
|
this.url = this.data.url;
|
|
|
|
if (this.data.listen) {
|
|
this.listenTask = makeTask(logger, {'listen': this.data.listen});
|
|
}
|
|
if (this.data.transcribe) {
|
|
this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe});
|
|
}
|
|
|
|
this.results = {};
|
|
this.bridged = false;
|
|
this.dials = new Map();
|
|
}
|
|
|
|
get name() { return TaskName.Dial; }
|
|
|
|
async exec(cs) {
|
|
super.exec(cs);
|
|
try {
|
|
if (cs.direction === CallDirection.Inbound) {
|
|
await this._initializeInbound(cs);
|
|
}
|
|
await this._attemptCalls(cs);
|
|
await this.awaitTaskDone();
|
|
this.performAction(this.method, this.results);
|
|
} catch (err) {
|
|
this.logger.error(`TaskDial:exec terminating with error ${err.message}`);
|
|
this.kill();
|
|
}
|
|
}
|
|
|
|
async kill() {
|
|
super.kill();
|
|
if (this.connectTime) {
|
|
const duration = moment().diff(this.connectTime, 'seconds');
|
|
this.results.dialCallDuration = duration;
|
|
this.logger.debug(`Dial:kill call ended after ${duration} seconds`);
|
|
}
|
|
|
|
this._killOutdials();
|
|
if (this.dlg) {
|
|
assert(this.ep);
|
|
if (this.dlg.connected) this.dlg.destroy();
|
|
debug(`Dial:kill deleting endpoint ${this.ep.uuid}`);
|
|
this.ep.destroy();
|
|
}
|
|
if (this.listenTask) await this.listenTask.kill();
|
|
if (this.transcribeTask) await this.transcribeTask.kill();
|
|
this.notifyTaskDone();
|
|
}
|
|
|
|
async _initializeInbound(cs) {
|
|
const {ep} = await cs.connectInboundCallToIvr(this.earlyMedia);
|
|
this.epOther = ep;
|
|
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
|
|
|
|
if (this.dialMusic) {
|
|
// play dial music to caller while we outdial
|
|
ep.play(this.dialMusic).catch((err) => {
|
|
this.logger.error(err, `TaskDial:_initializeInbound - error playing ${this.dialMusic}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
async _attemptCalls(cs) {
|
|
const {req, srf} = cs;
|
|
|
|
const sbcAddress = cs.direction === CallDirection.Inbound ?
|
|
`${req.source_address}:${req.source_port}` :
|
|
config.get('sbcAddress');
|
|
const opts = {
|
|
headers: this.headers,
|
|
proxy: `sip:${sbcAddress}`,
|
|
callingNumber: this.callerId || req.callingNumber
|
|
};
|
|
|
|
// construct bare-bones callInfo for the new outbound call attempt
|
|
const callInfo = Object.assign({}, cs.callInfo);
|
|
callInfo.parentCallSid = cs.callSid;
|
|
callInfo.direction = CallDirection.Outbound;
|
|
['callSid', 'callID', 'from', 'to', 'callerId', 'sipStatus', 'callStatus'].forEach((k) => delete callInfo[k]);
|
|
|
|
const ms = await cs.getMS();
|
|
this.target.forEach((t) => {
|
|
try {
|
|
t.url = t.url || this.url;
|
|
const sd = placeCall({
|
|
logger: this.logger,
|
|
application: cs.application,
|
|
srf,
|
|
ms,
|
|
sbcAddress,
|
|
target: t,
|
|
opts,
|
|
callInfo
|
|
});
|
|
this.dials.set(sd.callSid, sd);
|
|
|
|
sd
|
|
.on('callStatusChange', (obj) => {
|
|
switch (obj.callStatus) {
|
|
case CallStatus.Trying:
|
|
break;
|
|
case CallStatus.EarlyMedia:
|
|
if (this.target.length === 1 && !this.target[0].url && !this.dialMusic) {
|
|
this._bridgeEarlyMedia(sd);
|
|
}
|
|
break;
|
|
case CallStatus.InProgress:
|
|
this.logger.debug('Dial:_attemptCall -- call was answered');
|
|
break;
|
|
case CallStatus.Failed:
|
|
case CallStatus.Busy:
|
|
case CallStatus.NoAnswer:
|
|
this.dials.delete(sd.callSid);
|
|
if (this.dials.size === 0 && !this.connectTime) {
|
|
this.logger.debug('Dial:_attemptCalls - all calls failed after call failure, ending task');
|
|
this.kill();
|
|
}
|
|
break;
|
|
}
|
|
if (this.results.dialCallStatus !== CallStatus.Completed) {
|
|
Object.assign(this.results, {
|
|
dialCallStatus: obj.callStatus,
|
|
dialCallSid: sd.callSid,
|
|
});
|
|
}
|
|
})
|
|
.on('accept', () => {
|
|
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
|
this._connectSingleDial(cs, sd);
|
|
})
|
|
.on('decline', () => {
|
|
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
|
|
this.dials.delete(sd.callSid);
|
|
if (this.dials.size === 0 && !this.connectTime) {
|
|
this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending task');
|
|
this.kill();
|
|
}
|
|
});
|
|
} catch (err) {
|
|
this.logger.error(err, 'Dial:_attemptCalls');
|
|
}
|
|
});
|
|
}
|
|
|
|
_connectSingleDial(cs, sd) {
|
|
if (!this.bridged) {
|
|
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
|
|
this.epOther.api('uuid_break', this.epOther.uuid);
|
|
this.epOther.bridge(sd.ep);
|
|
this.bridged = true;
|
|
}
|
|
|
|
// ding! ding! ding! we have a winner
|
|
this._selectSingleDial(cs, sd);
|
|
this._killOutdials(); // NB: order is important
|
|
}
|
|
|
|
_selectSingleDial(cs, sd) {
|
|
this.connectTime = moment();
|
|
this.dials.delete(sd.callSid);
|
|
debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`);
|
|
this.ep = sd.ep;
|
|
this.dlg = sd.dlg;
|
|
this.callSid = sd.callSid;
|
|
if (this.earlyMedia) {
|
|
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
|
cs.propagateAnswer();
|
|
}
|
|
this.dlg.on('destroy', () => {
|
|
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
|
|
this.ep.unbridge();
|
|
this.kill();
|
|
});
|
|
|
|
Object.assign(this.results, {
|
|
dialCallStatus: CallStatus.Completed,
|
|
dialCallSid: sd.callSid,
|
|
});
|
|
|
|
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep, this);
|
|
if (this.listenTask) this.listenTask.exec(cs, this.ep, this);
|
|
}
|
|
|
|
_killOutdials() {
|
|
for (const [callSid, sd] of Array.from(this.dials)) {
|
|
this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`);
|
|
sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`));
|
|
}
|
|
this.dials.clear();
|
|
}
|
|
|
|
_bridgeEarlyMedia(sd) {
|
|
if (this.epOther && !this.bridged) {
|
|
this.epOther.api('uuid_break', this.epOther.uuid);
|
|
this.epOther.bridge(sd.ep);
|
|
this.bridged = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = TaskDial;
|