mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
major refactoring
This commit is contained in:
22
lib/session/call-info.js
Normal file
22
lib/session/call-info.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class CallInfo {
|
||||
constructor(opts) {
|
||||
this.callSid = opts.callSid;
|
||||
this.parentCallSid = opts.parentCallSid;
|
||||
this.direction = opts.direction;
|
||||
this.from = opts.from;
|
||||
this.to = opts.to;
|
||||
this.callId = opts.callId;
|
||||
this.sipStatus = opts.sipStatus;
|
||||
this.callStatus = opts.callStatus;
|
||||
this.callerId = opts.callerId;
|
||||
this.accountSid = opts.accountSid;
|
||||
this.applicationSid = opts.applicationSid;
|
||||
}
|
||||
|
||||
updateCallStatus(callStatus, sipStatus) {
|
||||
this.callStatus = callStatus;
|
||||
if (sipStatus) this.sipStatus = sipStatus;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallInfo;
|
||||
191
lib/session/call-session.js
Normal file
191
lib/session/call-session.js
Normal file
@@ -0,0 +1,191 @@
|
||||
const Emitter = require('events');
|
||||
const config = require('config');
|
||||
const {CallDirection, TaskPreconditions, CallStatus} = require('../utils/constants');
|
||||
const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
|
||||
class CallSession extends Emitter {
|
||||
constructor({logger, application, srf, tasks, callSid}) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.application = application;
|
||||
this.srf = srf;
|
||||
this.callSid = callSid;
|
||||
this.tasks = tasks;
|
||||
|
||||
this.callGone = false;
|
||||
}
|
||||
|
||||
async exec() {
|
||||
let idx = 0;
|
||||
this.logger.info(`CallSession:exec starting task list with ${this.tasks.length} tasks`);
|
||||
while (this.tasks.length && !this.callGone) {
|
||||
const task = this.tasks.shift();
|
||||
this.logger.debug(`CallSession:exec starting task #${++idx}: ${task.name}`);
|
||||
try {
|
||||
const resources = await this._evaluatePreconditions(task);
|
||||
this.currentTask = task;
|
||||
await task.exec(this, resources);
|
||||
this.currentTask = null;
|
||||
this.logger.debug(`CallSession:exec completed task #${idx}: ${task.name}`);
|
||||
} catch (err) {
|
||||
this.currentTask = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// all done - cleanup
|
||||
this.logger.info('CallSession:exec all tasks complete');
|
||||
this._onTasksDone();
|
||||
this._clearCalls();
|
||||
this.ms && this.ms.destroy();
|
||||
}
|
||||
|
||||
_onTasksDone() {
|
||||
// meant to be implemented by subclass if needed
|
||||
}
|
||||
|
||||
_callReleased() {
|
||||
this.logger.debug('CallSession:_callReleased - caller hung up');
|
||||
this.callGone = true;
|
||||
if (this.currentTask) this.currentTask.kill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the currently-executing application with a new application
|
||||
* NB: any tasks in the current stack that have not been executed are flushed
|
||||
*/
|
||||
replaceApplication(tasks) {
|
||||
this.tasks = tasks;
|
||||
this.logger.info(`CallSession:replaceApplication - set ${tasks.length} new tasks`);
|
||||
}
|
||||
_evaluatePreconditions(task) {
|
||||
switch (task.preconditions) {
|
||||
case TaskPreconditions.None:
|
||||
return;
|
||||
case TaskPreconditions.Endpoint:
|
||||
return this._evalEndpointPrecondition(task);
|
||||
case TaskPreconditions.StableCall:
|
||||
return this._evalStableCallPrecondition(task);
|
||||
case TaskPreconditions.UnansweredCall:
|
||||
return this._evalUnansweredCallPrecondition(task);
|
||||
default:
|
||||
assert(0, `invalid/unknown or missing precondition type ${task.preconditions} for task ${task.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
async _evalEndpointPrecondition(task) {
|
||||
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.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;
|
||||
|
||||
// we are going from an early media connection to answer
|
||||
await answerCall();
|
||||
}
|
||||
|
||||
try {
|
||||
// need to allocate an endpoint
|
||||
if (!this.ms) this.ms = await this.getMS();
|
||||
const ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
ep.cs = this;
|
||||
this.ep = ep;
|
||||
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
if (task.earlyMedia && !this.req.finalResponseSent) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
return ep;
|
||||
}
|
||||
answerCall();
|
||||
}
|
||||
else {
|
||||
// outbound call TODO
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
_evalStableCallPrecondition(task) {
|
||||
if (this.callGone) throw new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
if (!this.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered`);
|
||||
return this.dlg;
|
||||
}
|
||||
|
||||
_evalUnansweredCallPrecondition(task, callSid) {
|
||||
if (!this.req) throw new Error('invalid precondition unanswered_call for outbound call');
|
||||
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
if (this.req.finalResponseSent) {
|
||||
throw new Error(`${BADPRECONDITIONS}: final sip status already sent`);
|
||||
}
|
||||
return {req: this.req, res: this.res};
|
||||
}
|
||||
|
||||
_clearCalls() {
|
||||
if (this.dlg && this.dlg.connected) this.dlg.destroy();
|
||||
if (this.ep && this.ep.connected) this.ep.destroy();
|
||||
}
|
||||
|
||||
_callerHungup() {
|
||||
assert(false, 'subclass responsibility to override this method');
|
||||
}
|
||||
|
||||
async getMS() {
|
||||
if (!this.ms) {
|
||||
const mrf = this.srf.locals.mrf;
|
||||
this.ms = await mrf.connect(config.get('freeswitch'));
|
||||
}
|
||||
return this.ms;
|
||||
}
|
||||
|
||||
async createOrRetrieveEpAndMs() {
|
||||
const mrf = this.srf.locals.mrf;
|
||||
if (this.ms && this.ep) return {ms: this.ms, ep: this.ep};
|
||||
|
||||
// get a media server
|
||||
if (!this.ms) {
|
||||
this.ms = await mrf.connect(config.get('freeswitch'));
|
||||
}
|
||||
if (!this.ep) {
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
}
|
||||
return {ms: this.ms, ep: this.ep};
|
||||
}
|
||||
_notifyCallStatusChange(callStatus) {
|
||||
this.logger.debug({app: this.application}, `CallSession:_notifyCallStatusChange: ${JSON.stringify(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, `CallSession:_notifyCallStatusChange error sending ${JSON.stringify(callStatus)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallSession;
|
||||
26
lib/session/confirm-call-session.js
Normal file
26
lib/session/confirm-call-session.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const CallSession = require('./call-session');
|
||||
const {CallDirection} = require('../utils/constants');
|
||||
|
||||
class ConfirmCallSession extends CallSession {
|
||||
constructor({logger, application, dlg, ep, tasks}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
srf: dlg.srf,
|
||||
callSid: dlg.callSid,
|
||||
tasks
|
||||
});
|
||||
this.dlg = dlg;
|
||||
this.ep = ep;
|
||||
this.direction = CallDirection.Outbound;
|
||||
}
|
||||
|
||||
/**
|
||||
* empty implementation to override superclass so we do not delete dlg and ep
|
||||
*/
|
||||
_clearCalls() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ConfirmCallSession;
|
||||
119
lib/session/inbound-call-session.js
Normal file
119
lib/session/inbound-call-session.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const CallSession = require('./call-session');
|
||||
const {CallDirection, CallStatus} = require('../utils/constants');
|
||||
const hooks = require('../utils/notifiers');
|
||||
const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
|
||||
class InboundCallSession extends CallSession {
|
||||
constructor(req, res) {
|
||||
super({
|
||||
logger: req.locals.logger,
|
||||
srf: req.srf,
|
||||
application: req.locals.application,
|
||||
callSid: req.locals.callInfo.callSid,
|
||||
tasks: req.locals.application.tasks
|
||||
});
|
||||
this.req = req;
|
||||
this.res = res;
|
||||
this.srf = req.srf;
|
||||
this.logger = req.locals.logger;
|
||||
this.callInfo = req.locals.callInfo;
|
||||
this.direction = CallDirection.Inbound;
|
||||
const {notifyHook} = hooks(this.logger, this.callInfo);
|
||||
this.notifyHook = notifyHook;
|
||||
|
||||
req.on('cancel', this._callReleased.bind(this));
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
}
|
||||
|
||||
get speechSynthesisVendor() {
|
||||
return this.application.speech_synthesis_vendor;
|
||||
}
|
||||
get speechSynthesisVoice() {
|
||||
return this.application.speech_synthesis_voice;
|
||||
}
|
||||
|
||||
get speechRecognizerVendor() {
|
||||
return this.application.speech_recognizer_vendor;
|
||||
}
|
||||
get speechRecognizerLanguage() {
|
||||
return this.application.speech_recognizer_language;
|
||||
}
|
||||
|
||||
_onTasksDone() {
|
||||
if (!this.res.finalResponseSent) {
|
||||
this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite');
|
||||
this.res.send(603);
|
||||
}
|
||||
else if (this.dlg.connected) {
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('InboundCallSession:_onTasksDone hanging up call since all tasks are done');
|
||||
}
|
||||
}
|
||||
|
||||
async connectInboundCallToIvr(earlyMedia = false) {
|
||||
|
||||
// check for a stable inbound call already connected to the ivr
|
||||
if (this.ep && this.dlg) {
|
||||
this.logger.debug('CallSession:connectInboundCallToIvr - inbound call already connected to IVR');
|
||||
return {ep: this.ep, dlg: this.dlg};
|
||||
}
|
||||
|
||||
// check for an early media connection, where caller wants same
|
||||
if (this.ep && earlyMedia) {
|
||||
this.logger.debug('CallSession:connectInboundCallToIvr - inbound call already has early media connection');
|
||||
return {ep: this.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} = await this.createOrRetrieveEpAndMs();
|
||||
this.ep = ep;
|
||||
|
||||
if (earlyMedia) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
this.emit('callStatusChange', {sipStatus: 183, callStatus: CallStatus.EarlyMedia});
|
||||
return {ep, res: this.res};
|
||||
}
|
||||
const dlg = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
|
||||
dlg.on('destroy', this._callerHungup.bind(this));
|
||||
dlg.connectTime = moment();
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug(`CallSession:connectInboundCallToIvr - answered callSid ${this.callSid}`);
|
||||
this.ep = ep;
|
||||
this.dlg = dlg;
|
||||
return {ep, dlg};
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'CallSession:connectInboundCallToIvr error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async propagateAnswer() {
|
||||
if (!this.dlg) {
|
||||
assert(this.ep);
|
||||
this.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
|
||||
this.dlg.connectTime = moment();
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
|
||||
}
|
||||
}
|
||||
|
||||
_callerHungup() {
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('InboundCallSession: caller hung up');
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = InboundCallSession;
|
||||
Reference in New Issue
Block a user