changes for updateCall pause/resume listen audio

This commit is contained in:
Dave Horton
2020-02-08 14:16:05 -05:00
parent 3ca2d982cc
commit ff531e6964
12 changed files with 140 additions and 26 deletions

5
app.js
View File

@@ -4,7 +4,10 @@ const Mrf = require('drachtio-fsmrf');
srf.locals.mrf = new Mrf(srf); srf.locals.mrf = new Mrf(srf);
const config = require('config'); const config = require('config');
const PORT = process.env.HTTP_PORT || config.get('defaultHttpPort'); const PORT = process.env.HTTP_PORT || config.get('defaultHttpPort');
const logger = srf.locals.parentLogger = require('pino')(config.get('logging')); const opts = Object.assign({
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
}, config.get('logging'));
const logger = srf.locals.parentLogger = require('pino')(opts);
const installSrfLocals = require('./lib/utils/install-srf-locals'); const installSrfLocals = require('./lib/utils/install-srf-locals');
installSrfLocals(srf, logger); installSrfLocals(srf, logger);

View File

@@ -21,6 +21,7 @@
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 6379 "port": 6379
}, },
"defaultHttpPort": 3000,
"outdials": { "outdials": {
"drachtio": [ "drachtio": [
{ {

View File

@@ -1,13 +1,42 @@
const router = require('express').Router(); const router = require('express').Router();
const sysError = require('./error'); const sysError = require('./error');
const sessionTracker = require('../../session/session-tracker'); const sessionTracker = require('../../session/session-tracker');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../utils/errors');
const {CallStatus, CallDirection} = require('../../utils/constants');
/**
* validate the payload and retrieve the CallSession for the CallSid
*/
function retrieveCallSession(callSid, opts) {
if (opts.call_status_hook && !opts.call_hook) {
throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated');
}
const cs = sessionTracker.get(callSid);
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
}
else if (opts.call_status === CallStatus.NoAnswer) {
if (cs.direction === CallDirection.Outbound) {
if (!cs.isOutboundCallRinging) {
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
}
}
else {
if (cs.isInboundCallAnswered) {
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
}
}
}
return cs;
}
router.post('/:callSid', async(req, res) => { router.post('/:callSid', async(req, res) => {
const logger = req.app.locals.logger; const logger = req.app.locals.logger;
const callSid = req.params.callSid; const callSid = req.params.callSid;
logger.debug({body: req.body}, 'got upateCall request'); logger.debug({body: req.body}, 'got upateCall request');
try { try {
const cs = sessionTracker.get(callSid); const cs = retrieveCallSession(callSid, req.body);
if (!cs) { if (!cs) {
logger.info(`updateCall: callSid not found ${callSid}`); logger.info(`updateCall: callSid not found ${callSid}`);
return res.sendStatus(404); return res.sendStatus(404);

View File

@@ -6,7 +6,7 @@ const retrieveApp = require('./utils/retrieve-app');
const parseUrl = require('parse-url'); const parseUrl = require('parse-url');
module.exports = function(srf, logger) { module.exports = function(srf, logger) {
const {lookupAppByPhoneNumber} = srf.locals.dbHelpers; const {lookupAppByPhoneNumber, lookupApplicationBySid} = srf.locals.dbHelpers;
function initLocals(req, res, next) { function initLocals(req, res, next) {
const callSid = uuidv4(); const callSid = uuidv4();
@@ -14,6 +14,11 @@ module.exports = function(srf, logger) {
callSid, callSid,
logger: logger.child({callId: req.get('Call-ID'), callSid}) logger: logger.child({callId: req.get('Call-ID'), callSid})
}; };
if (req.has('X-Application-Sid')) {
const application_sid = req.get('X-Application-Sid');
req.locals.logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
req.locals.application_sid = application_sid;
}
next(); next();
} }
@@ -44,7 +49,13 @@ module.exports = function(srf, logger) {
async function retrieveApplication(req, res, next) { async function retrieveApplication(req, res, next) {
const logger = req.locals.logger; const logger = req.locals.logger;
try { try {
const app = await lookupAppByPhoneNumber(req.locals.calledNumber); let app;
if (req.locals.application_sid) {
app = await lookupApplicationBySid(req.locals.application_sid);
}
else {
app = await lookupAppByPhoneNumber(req.locals.calledNumber);
}
if (!app || !app.call_hook || !app.call_hook.url) { if (!app || !app.call_hook || !app.call_hook.url) {
logger.info(`rejecting call to ${req.locals.calledNumber}: no application or webhook url`); logger.info(`rejecting call to ${req.locals.calledNumber}: no application or webhook url`);
return res.send(480, { return res.send(480, {

View File

@@ -1,6 +1,6 @@
const Emitter = require('events'); const Emitter = require('events');
const config = require('config'); const config = require('config');
const {CallDirection, TaskPreconditions, CallStatus} = require('../utils/constants'); const {CallDirection, TaskPreconditions, CallStatus, TaskName} = require('../utils/constants');
const hooks = require('../utils/notifiers'); const hooks = require('../utils/notifiers');
const moment = require('moment'); const moment = require('moment');
const assert = require('assert'); const assert = require('assert');
@@ -63,6 +63,18 @@ class CallSession extends Emitter {
return this.application.speech_recognizer_language; return this.application.speech_recognizer_language;
} }
get hasStableDialog() {
return this.dlg && this.dlg.connected;
}
get isOutboundCallRinging() {
return this.direction === CallDirection.Outbound && this.req && !this.dlg;
}
get isInboundCallAnswered() {
return this.direction === CallDirection.Inbound && this.res.finalResponseSent;
}
async exec() { async exec() {
this.logger.info(`CallSession:exec starting task list with ${this.tasks.length} tasks`); this.logger.info(`CallSession:exec starting task list with ${this.tasks.length} tasks`);
while (this.tasks.length && !this.callGone) { while (this.tasks.length && !this.callGone) {
@@ -128,18 +140,45 @@ class CallSession extends Emitter {
async updateCall(opts) { async updateCall(opts) {
this.logger.debug(opts, 'CallSession:updateCall'); this.logger.debug(opts, 'CallSession:updateCall');
if (opts.call_status === 'completed' && this.dlg) {
if (opts.call_status === CallStatus.Completed && this.dlg) {
this.logger.info('CallSession:updateCall hanging up call due to request from api'); this.logger.info('CallSession:updateCall hanging up call due to request from api');
this._callerHungup(); this._callerHungup();
} }
else if (opts.call_status === CallStatus.NoAnswer) {
if (this.direction === CallDirection.Inbound) {
if (this.res && !this.res.finalResponseSent) {
this.res.send(503);
this._callReleased();
}
}
else {
if (this.req && !this.dlg) {
this.req.cancel();
this._callReleased();
}
}
}
else if (opts.call_hook && opts.call_hook.url) { else if (opts.call_hook && opts.call_hook.url) {
const hook = this.normalizeUrl(opts.call_hook.url, opts.call_hook.method, opts.call_hook.auth); const hook = this.normalizeUrl(opts.call_hook.url, opts.call_hook.method, opts.call_hook.auth);
this.logger.info({hook}, 'CallSession:updateCall replacing application due to request from api'); this.logger.info({hook}, 'CallSession:updateCall replacing application due to request from api');
const {actionHook} = hooks(this.logger, this.callInfo); const {actionHook} = hooks(this.logger, this.callInfo);
if (opts.call_status_hook) this.call_status_hook = opts.call_status_hook;
const tasks = await actionHook(hook); const tasks = await actionHook(hook);
this.logger.info({tasks}, 'CallSession:updateCall new task list'); this.logger.info({tasks}, 'CallSession:updateCall new task list');
this.replaceApplication(tasks); this.replaceApplication(tasks);
} }
else if (opts.listen_status) {
const task = this.currentTask;
if (!task || ![TaskName.Dial, TaskName.Listen].includes(task.name)) {
return this.logger.info(`CallSession:updateCall - disregarding listen_status in task ${task.name}`);
}
const listenTask = task.name === TaskName.Listen ? task : task.listenTask;
if (!listenTask) {
return this.logger.info('CallSession:updateCall - disregarding listen_status as Dial does not have a listen');
}
listenTask.updateListen(opts.listen_status);
}
} }
/** /**
@@ -262,6 +301,7 @@ class CallSession extends Emitter {
} }
return {ms: this.ms, ep: this.ep}; return {ms: this.ms, ep: this.ep};
} }
_notifyCallStatusChange({callStatus, sipStatus, duration}) { _notifyCallStatusChange({callStatus, sipStatus, duration}) {
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) || assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
(!duration && callStatus !== CallStatus.Completed), (!duration && callStatus !== CallStatus.Completed),

View File

@@ -26,7 +26,7 @@ class InboundCallSession extends CallSession {
this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite'); this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite');
this.res.send(603); this.res.send(603);
} }
else if (this.dlg.connected) { else if (this.dlg && this.dlg.connected) {
assert(this.dlg.connectTime); assert(this.dlg.connectTime);
const duration = moment().diff(this.dlg.connectTime, 'seconds'); const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});

View File

@@ -1,5 +1,5 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions, ListenEvents} = require('../utils/constants'); const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const moment = require('moment'); const moment = require('moment');
@@ -68,6 +68,23 @@ class TaskListen extends Task {
this.notifyTaskDone(); this.notifyTaskDone();
} }
updateListen(status) {
if (!this.killed && this.ep && this.ep.connected) {
this.logger.info(`TaskListen:updateListen status ${status}`);
switch (status) {
case ListenStatus.Pause:
this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
break;
case ListenStatus.Silence:
this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
break;
case ListenStatus.Resume:
this.ep.forkAudioResume().catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
break;
}
}
}
async _playBeep(ep) { async _playBeep(ep) {
await ep.play('tone_stream://L=1;%(500, 0, 1500)') await ep.play('tone_stream://L=1;%(500, 0, 1500)')
.catch((err) => this.logger.info(err, 'TaskListen:_playBeep Error playing beep')); .catch((err) => this.logger.info(err, 'TaskListen:_playBeep Error playing beep'));

View File

@@ -7,6 +7,7 @@ class TaskSay extends Task {
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.text = this.data.text; this.text = this.data.text;
this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia); this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
if (this.data.synthesizer) { if (this.data.synthesizer) {
this.voice = this.data.synthesizer.voice; this.voice = this.data.synthesizer.voice;
@@ -26,11 +27,14 @@ class TaskSay extends Task {
super.exec(cs); super.exec(cs);
this.ep = ep; this.ep = ep;
try { try {
while (!this.killed && this.loop--) {
this.logger.debug(`TaskSay: remaining loops ${this.loop}`);
await ep.speak({ await ep.speak({
ttsEngine: 'google_tts', ttsEngine: 'google_tts',
voice: this.voice || this.callSession.speechSynthesisVoice, voice: this.voice || this.callSession.speechSynthesisVoice,
text: this.text text: this.text
}); });
}
} catch (err) { } catch (err) {
this.logger.info(err, 'TaskSay:exec error'); this.logger.info(err, 'TaskSay:exec error');
} }

View File

@@ -30,6 +30,11 @@
"Inbound": "inbound", "Inbound": "inbound",
"Outbound": "outbound" "Outbound": "outbound"
}, },
"ListenStatus": {
"Pause": "pause",
"Silence": "silence",
"Resume": "resume"
},
"TaskPreconditions": { "TaskPreconditions": {
"None": "none", "None": "none",
"Endpoint": "endpoint", "Endpoint": "endpoint",

View File

@@ -6,7 +6,10 @@ const PORT = process.env.HTTP_PORT || config.get('defaultHttpPort');
function installSrfLocals(srf, logger) { function installSrfLocals(srf, logger) {
if (srf.locals.dbHelpers) return; if (srf.locals.dbHelpers) return;
const {lookupAppByPhoneNumber} = require('jambonz-db-helpers')(config.get('mysql'), logger); const {
lookupAppByPhoneNumber,
lookupApplicationBySid
} = require('jambonz-db-helpers')(config.get('mysql'), logger);
const { const {
updateCallStatus, updateCallStatus,
retrieveCall, retrieveCall,
@@ -17,6 +20,7 @@ function installSrfLocals(srf, logger) {
Object.assign(srf.locals, { Object.assign(srf.locals, {
dbHelpers: { dbHelpers: {
lookupAppByPhoneNumber, lookupAppByPhoneNumber,
lookupApplicationBySid,
updateCallStatus, updateCallStatus,
retrieveCall, retrieveCall,
listCalls, listCalls,

18
package-lock.json generated
View File

@@ -813,9 +813,9 @@
"integrity": "sha512-FKPAcEMJTYKDrd9DJUCc4VHnY/c65HOO9k8XqVNognF9T02hKEjGuBCM4Da9ipyfiHmVRuECwj0XNvZ361mkVQ==" "integrity": "sha512-FKPAcEMJTYKDrd9DJUCc4VHnY/c65HOO9k8XqVNognF9T02hKEjGuBCM4Da9ipyfiHmVRuECwj0XNvZ361mkVQ=="
}, },
"drachtio-fsmrf": { "drachtio-fsmrf": {
"version": "1.5.12", "version": "1.5.13",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-1.5.12.tgz", "resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-1.5.13.tgz",
"integrity": "sha512-pj+ozJ+eg9dQH9KNOwIx+BPLyBz5qt5YKIKk1svQU/iaU/w2fvW/+EgF7RlE7Ds/1soW9vkPJNSdq0GL25MOyA==", "integrity": "sha512-FC/Xifua4ut5tZ9cDRCaRoEIo7LEevh5gdqgzTyKo685gm10tO//Ln7Q6ZnVnbwpFOH4TxaIf+al25z/t0v6Cg==",
"requires": { "requires": {
"async": "^1.4.2", "async": "^1.4.2",
"debug": "^2.2.0", "debug": "^2.2.0",
@@ -2062,18 +2062,18 @@
} }
}, },
"jambonz-db-helpers": { "jambonz-db-helpers": {
"version": "0.2.0", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/jambonz-db-helpers/-/jambonz-db-helpers-0.2.0.tgz", "resolved": "https://registry.npmjs.org/jambonz-db-helpers/-/jambonz-db-helpers-0.2.4.tgz",
"integrity": "sha512-AykK4ICzUl5/LaNQGZdy8dlWuv8nOSSRVAqQDztJvdmJHyl4wTEC+///pKNgQlm+RX7R3vCV7dFCVoTHuIAx3A==", "integrity": "sha512-qfMKvXv//UDGFveOmeC3Xq2jMvTP7Y1P4F3EPf7VAgD10/ipozLRdEx+o3HlyF9wOeP3syha9ofpnel8VYLGLA==",
"requires": { "requires": {
"debug": "^4.1.1", "debug": "^4.1.1",
"mysql2": "^2.0.2" "mysql2": "^2.0.2"
} }
}, },
"jambonz-realtimedb-helpers": { "jambonz-realtimedb-helpers": {
"version": "0.1.3", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/jambonz-realtimedb-helpers/-/jambonz-realtimedb-helpers-0.1.3.tgz", "resolved": "https://registry.npmjs.org/jambonz-realtimedb-helpers/-/jambonz-realtimedb-helpers-0.1.6.tgz",
"integrity": "sha512-/lDhucxeR1h9wYvZ+P/UxjfzwTVxgD9IKtZWAJrBleYoLiK0MgTR2gdBThPZv7wbjU0apNcWen06Lf5nccnxQw==", "integrity": "sha512-5W7hRuPDCGeJfVLrweoNrfzQ7lCWy77+CcF4jqbTrbztZOK1rm0XhC1phCEUbghntmdLjTkwxpzEFxu7kyJKNQ==",
"requires": { "requires": {
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"debug": "^4.1.1", "debug": "^4.1.1",

View File

@@ -29,12 +29,12 @@
"config": "^3.2.4", "config": "^3.2.4",
"debug": "^4.1.1", "debug": "^4.1.1",
"drachtio-fn-b2b-sugar": "0.0.12", "drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-fsmrf": "^1.5.12", "drachtio-fsmrf": "^1.5.13",
"drachtio-srf": "^4.4.27", "drachtio-srf": "^4.4.27",
"express": "^4.17.1", "express": "^4.17.1",
"ip": "^1.1.5", "ip": "^1.1.5",
"jambonz-db-helpers": "^0.2.0", "jambonz-db-helpers": "^0.2.4",
"jambonz-realtimedb-helpers": "0.1.3", "jambonz-realtimedb-helpers": "0.1.6",
"moment": "^2.24.0", "moment": "^2.24.0",
"parse-url": "^5.0.1", "parse-url": "^5.0.1",
"pino": "^5.14.0", "pino": "^5.14.0",