work on say and gather

This commit is contained in:
Dave Horton
2020-01-13 14:01:19 -05:00
parent 1debb193c2
commit 1a656f3f0e
16 changed files with 1034 additions and 236 deletions

View File

@@ -1,5 +1,15 @@
const Emitter = require('events');
const config = require('config');
const TaskList = require('./task-list');
const request = require('request');
const notifiers = require('./utils/notifiers');
const {CallStatus, CallDirection, TaskPreconditions} = require('./utils/constants');
//require('request-debug')(request);
const makeTask = require('./tasks/make_task');
const resourcesMixin = require('./utils/resources');
const moment = require('moment');
const assert = require('assert');
const BADPRECONDITIONS = 'preconditions not met';
class CallSession extends Emitter {
constructor(req, res) {
@@ -9,47 +19,144 @@ class CallSession extends Emitter {
this.srf = req.srf;
this.logger = req.locals.logger;
this.application = req.locals.application;
this.statusCallback = this.application.call_status_hook;
this.statusCallbackMethod = this.application.status_hook_http_method || 'POST';
this.idxTask = 0;
this.resources = new Map();
this.direction = CallDirection.Inbound;
this.callAttributes = req.locals.callAttributes;
// array of TaskLists, the one currently executing is at the front
this._executionStack = [new TaskList(this.application.tasks, this.callSid)];
this.childCallSids = [];
this.calls = new Map();
this.calls.set(this.parentCallSid, {ep: null, dlg: null});
this.hooks = notifiers(this.logger, this.callAttributes);
req.on('cancel', this._onCallerHangup.bind(this));
this.on('callStatusChange', this._onCallStatusChange.bind(this));
}
get callSid() { return this.callAttributes.CallSid; }
get parentCallSid() { return this.callAttributes.CallSid; }
get actionHook() { return this.hooks.actionHook; }
async exec() {
let idx = 0;
for (const task of this.application.tasks) {
try {
this.logger.debug(`CallSession: executing task #${++idx}: ${task.name}`);
const continueOn = await task.exec(this);
if (!continueOn) break;
} catch (err) {
this.logger.error({err, task}, 'Error executing task');
while (this._executionStack.length) {
const taskList = this.currentTaskList = this._executionStack.shift();
this.logger.debug(`CallSession:exec starting task list with ${taskList.tasks.length} tasks`);
while (taskList.length) {
const {task, callSid} = taskList.shift();
this.logger.debug(`CallSession:exec starting task #${++idx}: ${task.name}`);
try {
const resources = await this._evaluatePreconditions(task, callSid);
await task.exec(this, resources);
this.logger.debug(`CallSession:exec completed task #${idx}: ${task.name}`);
} catch (err) {
if (err.message.includes(BADPRECONDITIONS)) {
this.logger.info(`CallSession:exec task #${idx}: ${task.name}: ${err.message}`);
}
else {
this.logger.error(err, `Error executing task #${idx}: ${task.name}`);
break;
}
}
}
this.logger.info('CallSession: finished all tasks');
if (!this.res.finalResponseSent) {
this.logger.info('CallSession: auto-generating non-success response to invite');
this.res.send(603);
}
this._clearResources();
}
// all done - cleanup
this.logger.info('CallSession:exec finished all tasks');
if (!this.res.finalResponseSent) {
this.logger.info('CallSession:exec auto-generating non-success response to invite');
this.res.send(603);
}
this._clearCalls();
this.clearResources(); // still needed? ms may be only thing in here
}
_evaluatePreconditions(task, callSid) {
switch (task.preconditions) {
case TaskPreconditions.None:
return;
case TaskPreconditions.Endpoint:
return this._evalEndpointPrecondition(task, callSid);
case TaskPreconditions.StableCall:
return this._evalStableCallPrecondition(task, callSid);
case TaskPreconditions.UnansweredCall:
return this._evalUnansweredCallPrecondition(task, callSid);
default:
assert(0, `invalid/unknown or missing precondition type ${task.preconditions} for task ${task.name}`);
}
}
addResource(name, resource) {
this.logger.debug(`CallSession:addResource: adding ${name}`);
this.resources.set(name, resource);
async _evalEndpointPrecondition(task, callSid) {
const resources = this.calls.get(callSid);
if (!resources) throw new Error(`task ${task.name} attempting to operate on unknown CallSid ${callSid}`);
if (resources.ep) return resources.ep;
try {
// need to allocate an endpoint
const mrf = this.srf.locals.mrf;
let ms = this.getResource('ms');
if (!ms) {
ms = await mrf.connect(config.get('freeswitch'));
this.addResource('ms', ms);
}
const ep = await ms.createEndpoint({remoteSdp: this.req.body});
resources.ep = ep;
if (task.earlyMedia && callSid === this.parentCallSid && this.req && !this.req.finalResponseSent) {
this.res.send(183, {body: ep.local.sdp});
this.calls.set(callSid, resources);
return ep;
}
const uas = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
resources.dlg = uas;
this.calls.set(callSid, resources);
return ep;
} catch (err) {
this.logger.error(err, `Error attempting to allocate endpoint for for task ${task.name}`);
throw new Error(`${BADPRECONDITIONS}: unable to allocate endpoint - callSid ${callSid}`);
}
}
getResource(name) {
return this.resources.get(name);
_evalStableCallPrecondition(task, callSid) {
const resources = this.calls.get(callSid);
if (!resources) throw new Error(`task ${task.name} attempting to operate on unknown callSid ${callSid}`);
if (resources.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered - callSid ${callSid}`);
return resources.dlg;
}
removeResource(name) {
this.logger.debug(`CallSession:removeResource: removing ${name}`);
this.resources.delete(name);
_evalUnansweredCallPrecondition(task, callSid) {
if (callSid !== this.parentCallSid || !this.req) {
throw new Error(`${BADPRECONDITIONS}: no inbound call - callSid ${callSid}`);
}
if (this.req.finalResponseSent) {
throw new Error(`${BADPRECONDITIONS}: final sip status already sent - callSid ${callSid}`);
}
return {req: this.req, res: this.res};
}
async createOrRetrieveEpAndMs(remoteSdp) {
_clearCalls() {
for (const [callSid, resources] of Array.from(this.calls).reverse()) {
try {
this.logger.debug(`CallSession:_clearCalls clearing call sid ${callSid}`);
[resources.ep, resources.dlg].forEach((r) => {
if (r && r.connected) r.destroy();
});
} catch (err) {
this.logger.error(err, `clearResources: clearing call sid ${callSid}`);
}
}
this.calls.clear();
}
/**
* retrieve the media server and endpoint for this call, allocate them if needed
*/
async createOrRetrieveEpAndMs() {
const mrf = this.srf.locals.mrf;
let ms = this.getResource('ms');
let ep = this.getResource('epIn');
@@ -61,34 +168,136 @@ class CallSession extends Emitter {
this.addResource('ms', ms);
}
if (!ep) {
ep = await ms.createEndpoint({remoteSdp});
ep = await ms.createEndpoint({remoteSdp: this.req.body});
this.addResource('epIn', ep);
}
return {ms, ep};
}
/**
* clear down resources
* (note: we remove in reverse order they were added since mediaserver
* is typically added first and I prefer to destroy it after any resources it holds)
*/
_clearResources() {
for (const [name, resource] of Array.from(this.resources).reverse()) {
try {
this.logger.info(`CallSession:_clearResources: deleting ${name}`);
if (resource.connected) resource.destroy();
} catch (err) {
this.logger.error(err, `CallSession:_clearResources: error deleting ${name}`);
async connectInboundCallToIvr(earlyMedia = false) {
// if this is not an inbound call scenario, nothing to do
if (!this.parentCallSid) {
this.logger.debug('CallSession:connectInboundCallToIvr - session was not triggered by an inbound call');
return;
}
// check for a stable inbound call already connected to the ivr
const ms = this.getResource('ms');
const resources = this.calls.get(this.parentCallSid);
if (ms && resources.ep && resources.dlg) {
this.logger.debug('CallSession:connectInboundCallToIvr - inbound call already connected to IVR');
return {ms, ep: resources.ep, dlg: resources.dlg};
}
// check for an early media connection, where caller wants same
if (ms && resources.ep && earlyMedia) {
this.logger.debug('CallSession:connectInboundCallToIvr - inbound call already has early media connection');
return {ms, ep: resources.ep};
}
// ok, we need to connect the inbound call to the ivr
try {
assert(!this.req.finalResponseSent);
this.logger.debug('CallSession:connectInboundCallToIvr - creating endpoint for inbound call');
const {ep, ms} = await this.createOrRetrieveEpAndMs();
if (earlyMedia) {
this.res.send(183, {body: ep.local.sdp});
this.calls.set(this.parentCallSid, {ep});
return {ep, ms, res: this.res};
}
const dlg = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
this.calls.set(this.parentCallSid, {ep, dlg});
return {ep, ms, dlg};
} catch (err) {
this.logger.error(err, 'CallSession:connectInboundCallToIvr error');
throw err;
}
}
async answerParentCall(remoteSdp) {
assert(this.parentCallSid, 'CallSession:answerParentCall - no parent call sid');
const resources = this.calls.get(this.parentCallSid);
resources.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: remoteSdp});
resources.set(this.parentCallSid, resources);
}
/**
* allocate a new endpoint for this call, caller's responsibility to destroy
*/
async createEndpoint(remoteSdp) {
try {
let ms = this.getResource('ms');
if (!ms) {
const mrf = this.srf.locals.mrf;
ms = await mrf.connect(config.get('freeswitch'));
this.addResource('ms', ms);
}
const ep = await ms.createEndpoint({remoteSdp});
return ep;
} catch (err) {
this.logger.error(err, `CallSession:createEndpoint: error creating endpoint for remoteSdp ${remoteSdp}`);
throw err;
}
}
/**
* Replace the currently-executing application with a new application
* NB: any tasks in the current stack that have not been executed are flushed
* @param {object|array} payload - new application to execute
*/
replaceApplication(payload) {
const taskData = Array.isArray(payload) ? payload : [payload];
const tasks = [];
for (const t in taskData) {
try {
const task = makeTask(this.logger, taskData[t]);
tasks.push(task);
} catch (err) {
this.logger.info({data: taskData[t]}, `invalid web callback payload: ${err.message}`);
return;
}
}
this.application.tasks = tasks;
this.idxTask = 0;
this.logger.debug(`CallSession:replaceApplication - set ${tasks.length} new tasks`);
}
/**
* got CANCEL from inbound leg
*/
_onCallerHangup(evt) {
this.logger.debug('CallSession: caller hung before connection');
}
/**
* got BYE from inbound leg
*/
_onCallStatusChange(evt) {
this.logger.debug(evt, 'CallSession:_onCallStatusChange');
if (this.statusCallback) {
if (evt.status === CallStatus.InProgress) this.connectTime = moment();
const params = Object.assign(this.callAttributes, {CallStatus: evt.status, SipStatus: evt.sipStatus});
if (evt.status === CallStatus.Completed) {
const duration = moment().diff(this.connectTime, 'seconds');
this.logger.debug(`CallSession:_onCallStatusChange duration was ${duration}`);
Object.assign(params, {Duration: duration});
}
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(`Error sending call status to ${this.statusCallback}: ${err.message}`);
});
}
}
}
Object.assign(CallSession.prototype, resourcesMixin);
module.exports = CallSession;