added support for conference verb

This commit is contained in:
Dave Horton
2020-04-27 11:25:39 -04:00
parent d31c53d383
commit 8ee590172b
27 changed files with 888 additions and 133 deletions

View File

@@ -5,7 +5,7 @@ const moment = require('moment');
const assert = require('assert');
const sessionTracker = require('./session-tracker');
const makeTask = require('../tasks/make_task');
const normalizeJamones = require('../utils/normalize-jamones');
const normalizeJambones = require('../utils/normalize-jambones');
const listTaskNames = require('../utils/summarize-tasks');
const BADPRECONDITIONS = 'preconditions not met';
@@ -43,7 +43,8 @@ class CallSession extends Emitter {
this.tmpFiles = new Set();
sessionTracker.add(this.callSid, this);
// if this is a ConfirmSession
if (!this.isConfirmCallSession) sessionTracker.add(this.callSid, this);
}
/**
@@ -143,6 +144,27 @@ class CallSession extends Emitter {
return this.direction === CallDirection.Inbound && this.res.finalResponseSent;
}
/**
* returns the account sid
*/
get accountSid() {
return this.callInfo.accountSid;
}
/**
* returns true if this session was transferred from another server
*/
get isTransferredCall() {
return this.application.transferredCall === true;
}
/**
* returns true if this session is a ConfirmCallSession
*/
get isConfirmCallSession() {
return this.constructor.name === 'ConfirmCallSession';
}
/**
* execute the tasks in the CallSession. The tasks are executed in sequence until
* they complete, or the caller hangs up.
@@ -178,7 +200,7 @@ class CallSession extends Emitter {
this._onTasksDone();
this._clearResources();
sessionTracker.remove(this.callSid);
if (!this.isConfirmCallSession) sessionTracker.remove(this.callSid);
}
trackTmpFile(path) {
@@ -224,7 +246,7 @@ class CallSession extends Emitter {
this.logger.debug('CallSession:_callReleased - caller hung up');
this.callGone = true;
if (this.currentTask) {
this.currentTask.kill();
this.currentTask.kill(this);
this.currentTask = null;
}
}
@@ -265,7 +287,7 @@ class CallSession extends Emitter {
const tasks = await this.requestor.request(opts.call_hook, this.callInfo);
if (tasks && tasks.length > 0) {
this.logger.info({tasks: listTaskNames(tasks)}, 'CallSession:updateCall new task list');
this.replaceApplication(normalizeJamones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
this.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
}
}
@@ -314,15 +336,15 @@ class CallSession extends Emitter {
if (typeof whisper === 'string' || (typeof whisper === 'object' && whisper.url)) {
// retrieve a url
const json = await this.requestor(opts.call_hook, this.callInfo);
tasks = normalizeJamones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
}
else if (Array.isArray(whisper)) {
// an inline array of tasks
tasks = normalizeJamones(this.logger, whisper).map((tdata) => makeTask(this.logger, tdata));
tasks = normalizeJambones(this.logger, whisper).map((tdata) => makeTask(this.logger, tdata));
}
else if (typeof whisper === 'object') {
// a single task
tasks = normalizeJamones(this.logger, [whisper]).map((tdata) => makeTask(this.logger, tdata));
tasks = normalizeJambones(this.logger, [whisper]).map((tdata) => makeTask(this.logger, tdata));
}
else {
this.logger.info({opts}, 'CallSession:_lccWhisper invalid options were provided');
@@ -406,6 +428,18 @@ class CallSession extends Emitter {
this.currentTask = null;
}
}
kill() {
if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)');
else this.logger.info('CallSession:kill');
if (this.currentTask) {
this.currentTask.kill();
this.currentTask = null;
}
this.tasks = [];
this.taskIdx = 0;
}
_evaluatePreconditions(task) {
switch (task.preconditions) {
case TaskPreconditions.None:
@@ -426,19 +460,9 @@ class CallSession extends Emitter {
* @param {Task} task - task to be executed
*/
async _evalEndpointPrecondition(task) {
this.logger.debug('_evalEndpointPrecondition');
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
const answerCall = async() => {
const uas = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
uas.on('destroy', this._callerHungup.bind(this));
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');
};
if (this.ep) {
if (!task.earlyMedia || this.dlg) return this.ep;
@@ -454,12 +478,15 @@ class CallSession extends Emitter {
ep.cs = this;
this.ep = ep;
this.logger.debug('allocated endpoint');
if (this.direction === CallDirection.Inbound) {
if (task.earlyMedia && !this.req.finalResponseSent) {
this.res.send(183, {body: ep.local.sdp});
return ep;
}
this.propagateAnswer();
this.logger.debug('propogating answer');
await this.propagateAnswer();
}
else {
// outbound call TODO
@@ -495,6 +522,23 @@ class CallSession extends Emitter {
return {req: this.req, res: this.res};
}
/**
* Discard the current endpoint and allocate a new one, connecting the dialog to it.
* This is used, for instance, from the Conference verb when a caller has been
* kicked out of conference when a moderator leaves -- the endpoint is destroyed
* as well, but the app may want to continue on with other actions
*/
async replaceEndpoint() {
if (!this.dlg) {
this.logger.error('CallSession:replaceEndpoint cannot be called without stable dlg');
return;
}
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
await this.dlg.modify(this.ep.local.sdp);
this.logger.debug('CallSession:replaceEndpoint completed');
return this.ep;
}
/**
* Hang up the call and free the media endpoint
*/
@@ -543,13 +587,14 @@ class CallSession extends Emitter {
/**
* 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.logger.debug('answered call');
this.dlg.on('destroy', this._callerHungup.bind(this));
this.wrapDialog(this.dlg);
this.dlg.callSid = this.callSid;
@@ -590,6 +635,45 @@ class CallSession extends Emitter {
return {ms: this.ms, ep: this.ep};
}
/**
* A conference that the current task is waiting on has just started
* @param {*} opts
*/
notifyStartConference(opts) {
if (this.currentTask && typeof this.currentTask.notifyStartConference === 'function') {
this.currentTask.notifyStartConference(this, opts);
}
}
/**
* Transfer the call to another feature server
* @param {uri} sip uri to refer the call to
*/
async referCall(referTo) {
assert (this.hasStableDialog);
const res = await this.dlg.request({
method: 'REFER',
headers: {
'Refer-To': referTo,
'Referred-By': `sip:${this.srf.locals.localSipAddress}`
}
});
return [200, 202].includes(res.status);
}
getRemainingTaskData() {
const tasks = [...this.tasks];
tasks.unshift(this.currentTask);
const remainingTasks = [];
for (const task of tasks) {
const o = {};
o[task.name] = task.toJSON();
remainingTasks.push(o);
}
return remainingTasks;
}
/**
* 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