mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-19 04:17:44 +00:00
add support for live call control
This commit is contained in:
47
README.md
47
README.md
@@ -19,15 +19,13 @@ the `drachtio` object specifies the port to listen on for tcp connections from d
|
||||
|
||||
##### freeswitch location
|
||||
```
|
||||
"freeswitch: [
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"port": 8021,
|
||||
"secret": "ClueCon"
|
||||
}
|
||||
],
|
||||
"freeswitch: {
|
||||
"address": "127.0.0.1",
|
||||
"port": 8021,
|
||||
"secret": "ClueCon"
|
||||
},
|
||||
```
|
||||
the `freeswitch` property specifies an array of freeswitch servers to use to handle incoming calls.
|
||||
the `freeswitch` property specifies the location of the freeswitch server to use for media handling.
|
||||
|
||||
##### application log level
|
||||
```
|
||||
@@ -39,12 +37,43 @@ the `freeswitch` property specifies an array of freeswitch servers to use to han
|
||||
Login credentials for the mysql server databas.
|
||||
```
|
||||
"mysql": {
|
||||
"host": "localhost",
|
||||
"host": "127.0.0.1",
|
||||
"user": "jambones",
|
||||
"password": "jambones",
|
||||
"database": "jambones"
|
||||
}
|
||||
```
|
||||
##### redis server location
|
||||
Login credentials for the redis server databas.
|
||||
```
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379
|
||||
}
|
||||
```
|
||||
|
||||
##### port to listen on for HTTP API requests
|
||||
The HTTP listen port can be set by the `HTTP_PORT` environment variable, but it not set the default port will be taken from the configuration file.
|
||||
|
||||
```
|
||||
"defaultHttpPort": 3000,
|
||||
```
|
||||
|
||||
##### REST-initiated outdials
|
||||
When an outdial is triggered via the REST API, the application needs to select a drachtio sip server to generate the INVITE, and it needs to know the IP addresses of the SBC(s) to send the outbound call through. Both are provided as arrays in the configuration file, and if more than one is supplied they will be used in a round-robin fashion.
|
||||
|
||||
```
|
||||
"outdials": {
|
||||
"drachtio": [
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"port": 9022,
|
||||
"secret": "cymru"
|
||||
}
|
||||
],
|
||||
"sbc": ["127.0.0.1:5060"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Running the test suite
|
||||
The test suite currently only consists of JSON-parsing unit tests. A full end-to-end sip test suite should be added.
|
||||
|
||||
15
app.js
15
app.js
@@ -3,9 +3,11 @@ const srf = new Srf();
|
||||
const Mrf = require('drachtio-fsmrf');
|
||||
srf.locals.mrf = new Mrf(srf);
|
||||
const config = require('config');
|
||||
const PORT = process.env.HTTP_PORT || config.get('defaultHttpPort');
|
||||
const logger = srf.locals.parentLogger = require('pino')(config.get('logging'));
|
||||
const {lookupAppByPhoneNumber} = require('jambonz-db-helpers')(config.get('mysql'), logger);
|
||||
srf.locals.dbHelpers = {lookupAppByPhoneNumber};
|
||||
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
||||
installSrfLocals(srf, logger);
|
||||
|
||||
const {
|
||||
initLocals,
|
||||
normalizeNumbers,
|
||||
@@ -13,8 +15,8 @@ const {
|
||||
invokeWebCallback
|
||||
} = require('./lib/middleware')(srf, logger);
|
||||
|
||||
// HTTP
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
|
||||
// HTTP
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
app.locals.logger = logger;
|
||||
@@ -22,6 +24,7 @@ const httpRoutes = require('./lib/http-routes');
|
||||
|
||||
const InboundCallSession = require('./lib/session/inbound-call-session');
|
||||
|
||||
|
||||
// disable logging in test mode
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const noop = () => {};
|
||||
@@ -64,6 +67,6 @@ app.use((err, req, res, next) => {
|
||||
});
|
||||
app.listen(PORT);
|
||||
|
||||
logger.info(`listening for HTTP requests on port ${PORT}`);
|
||||
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
|
||||
|
||||
module.exports = {srf};
|
||||
module.exports = {srf, logger};
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
"password": "jambones",
|
||||
"database": "jambones"
|
||||
},
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379
|
||||
},
|
||||
"outdials": {
|
||||
"drachtio": [
|
||||
{
|
||||
|
||||
@@ -11,21 +11,51 @@ const Srf = require('drachtio-srf');
|
||||
const drachtio = config.get('outdials.drachtio');
|
||||
const sbcs = config.get('outdials.sbc');
|
||||
const Mrf = require('drachtio-fsmrf');
|
||||
const installSrfLocals = require('../../utils/install-srf-locals');
|
||||
let idxDrachtio = 0;
|
||||
let idxSbc = 0;
|
||||
let srfs = [];
|
||||
let initializedSrfs = false;
|
||||
|
||||
const srfs = drachtio.map((d) => {
|
||||
const srf = new Srf();
|
||||
srf.connect(d);
|
||||
srf
|
||||
.on('connect', (err, hp) => {
|
||||
if (!err) console.log(`Connected to drachtio at ${hp} for REST outdials`);
|
||||
else console.log(`Error connecting to drachtio for outdials: ${err}`);
|
||||
srf.locals.mrf = new Mrf(srf);
|
||||
})
|
||||
.on('error', (err) => console.log(err));
|
||||
return srf;
|
||||
});
|
||||
/**
|
||||
* Connect to a single drachtio server, returning a Promise when connected.
|
||||
* Upon connect, add ourselves to the list of active servers, removing if we lose the connection
|
||||
*/
|
||||
function connectSrf(logger, d) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const srf = new Srf();
|
||||
srf.connect(d);
|
||||
srf
|
||||
.on('connect', (err, hp) => {
|
||||
if (!err) logger.info(`connectSrf: Connected to drachtio at ${hp} for REST outdials`);
|
||||
else logger.error(`connectSrf: Error connecting to drachtio for outdials: ${err}`);
|
||||
srf.locals.mrf = new Mrf(srf);
|
||||
installSrfLocals(srf, logger);
|
||||
srfs.push(srf);
|
||||
resolve(srf);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
logger.error(err, 'connectSrf error');
|
||||
srfs = srfs.filter((s) => s !== srf);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a connection to a drachtio server, lazily creating when first called
|
||||
*/
|
||||
function getSrfForOutdial(logger) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (srfs.length === 0 && initializedSrfs) return reject('no available drachtio servers for outdial');
|
||||
else if (srfs.length > 0) return resolve(srfs[idxDrachtio++ % srfs.length]);
|
||||
else {
|
||||
logger.debug(drachtio, 'getSrfForOutdial - attempting to connect');
|
||||
initializedSrfs = true;
|
||||
resolve(Promise.race(drachtio.map((d) => connectSrf(logger, d))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function validate(logger, payload) {
|
||||
const data = Object.assign({}, {
|
||||
@@ -57,7 +87,7 @@ router.post('/', async(req, res) => {
|
||||
let uri, cs, to;
|
||||
const restDial = await validate(logger, req.body);
|
||||
const sbcAddress = sbcs[idxSbc++ % sbcs.length];
|
||||
const srf = srfs[idxDrachtio++ % srfs.length];
|
||||
const srf = await getSrfForOutdial(logger);
|
||||
const target = restDial.to;
|
||||
const opts = {
|
||||
'callingNumber': restDial.from
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const api = require('express').Router();
|
||||
|
||||
api.use('/createCall', require('./create-call'));
|
||||
api.use('/updateCall', require('./update-call'));
|
||||
|
||||
module.exports = api;
|
||||
|
||||
22
lib/http-routes/api/update-call.js
Normal file
22
lib/http-routes/api/update-call.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('./error');
|
||||
const sessionTracker = require('../../session/session-tracker');
|
||||
|
||||
router.post('/:callSid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const callSid = req.params.callSid;
|
||||
logger.debug({body: req.body}, 'got upateCall request');
|
||||
try {
|
||||
const cs = sessionTracker.get(callSid);
|
||||
if (!cs) {
|
||||
logger.info(`updateCall: callSid not found ${callSid}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.sendStatus(202);
|
||||
cs.updateCall(req.body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -16,7 +16,7 @@ class CallInfo {
|
||||
this.callId = req.get('Call-ID');
|
||||
this.sipStatus = 100;
|
||||
this.callStatus = CallStatus.Trying;
|
||||
this.originatingSipIP = req.get('X-Forwarded-For');
|
||||
this.originatingSipIp = req.get('X-Forwarded-For');
|
||||
this.originatingSipTrunkName = req.get('X-Originating-Carrier');
|
||||
}
|
||||
else if (opts.parentCallInfo) {
|
||||
@@ -74,7 +74,7 @@ class CallInfo {
|
||||
accountSid: this.accountSid,
|
||||
applicationSid: this.applicationSid
|
||||
};
|
||||
['parentCallSid', 'originatingSipIP', 'originatingSipTrunkName'].forEach((prop) => {
|
||||
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
|
||||
if (this[prop]) obj[prop] = this[prop];
|
||||
});
|
||||
if (typeof this.duration === 'number') obj.duration = this.duration;
|
||||
|
||||
@@ -4,6 +4,7 @@ const {CallDirection, TaskPreconditions, CallStatus} = require('../utils/constan
|
||||
const hooks = require('../utils/notifiers');
|
||||
const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
const sessionTracker = require('./session-tracker');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
|
||||
class CallSession extends Emitter {
|
||||
@@ -18,9 +19,14 @@ class CallSession extends Emitter {
|
||||
const {notifyHook} = hooks(this.logger, this.callInfo);
|
||||
this.notifyHook = notifyHook;
|
||||
|
||||
this.updateCallStatus = srf.locals.dbHelpers.updateCallStatus;
|
||||
this.serviceUrl = srf.locals.serviceUrl;
|
||||
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx = 0;
|
||||
this.callGone = false;
|
||||
|
||||
sessionTracker.add(this.callSid, this);
|
||||
}
|
||||
|
||||
get callSid() {
|
||||
@@ -87,6 +93,8 @@ class CallSession extends Emitter {
|
||||
this._onTasksDone();
|
||||
this._clearCalls();
|
||||
this.ms && this.ms.destroy();
|
||||
|
||||
sessionTracker.remove(this.callSid);
|
||||
}
|
||||
|
||||
_onTasksDone() {
|
||||
@@ -96,7 +104,42 @@ class CallSession extends Emitter {
|
||||
_callReleased() {
|
||||
this.logger.debug('CallSession:_callReleased - caller hung up');
|
||||
this.callGone = true;
|
||||
if (this.currentTask) this.currentTask.kill();
|
||||
if (this.currentTask) {
|
||||
this.currentTask.kill();
|
||||
this.currentTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
normalizeUrl(url, method, auth) {
|
||||
const hook = {url, method};
|
||||
if (auth && auth.username && auth.password) Object.assign(hook, auth);
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
const or = this.originalRequest;
|
||||
if (or) {
|
||||
hook.url = `${or.baseUrl}${url}`;
|
||||
hook.method = hook.method || or.method || 'POST';
|
||||
if (!hook.auth && or.auth) Object.assign(hook, or.auth);
|
||||
}
|
||||
}
|
||||
this.logger.debug({hook}, 'Task:normalizeUrl');
|
||||
return hook;
|
||||
}
|
||||
|
||||
async updateCall(opts) {
|
||||
this.logger.debug(opts, 'CallSession:updateCall');
|
||||
if (opts.call_status === 'completed' && this.dlg) {
|
||||
this.logger.info('CallSession:updateCall hanging up call due to request from api');
|
||||
this._callerHungup();
|
||||
}
|
||||
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);
|
||||
this.logger.info({hook}, 'CallSession:updateCall replacing application due to request from api');
|
||||
const {actionHook} = hooks(this.logger, this.callInfo);
|
||||
const tasks = await actionHook(hook);
|
||||
this.logger.info({tasks}, 'CallSession:updateCall new task list');
|
||||
this.replaceApplication(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,9 +148,14 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
replaceApplication(tasks) {
|
||||
this.tasks = tasks;
|
||||
this.logger.info({tasks}, `CallSession:replaceApplication - reset application with ${tasks.length} new tasks`);
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx++;
|
||||
this.logger.info({tasks},
|
||||
`CallSession:replaceApplication reset with ${tasks.length} new tasks, stack depth is ${this.stackIdx}`);
|
||||
if (this.currentTask) {
|
||||
this.currentTask.kill();
|
||||
this.currentTask = null;
|
||||
}
|
||||
}
|
||||
_evaluatePreconditions(task) {
|
||||
switch (task.preconditions) {
|
||||
@@ -227,6 +275,9 @@ class CallSession extends Emitter {
|
||||
} catch (err) {
|
||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
// update calls db
|
||||
this.updateCallStatus(this.callInfo, this.serviceUrl).catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
lib/session/session-tracker.js
Normal file
42
lib/session/session-tracker.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const Emitter = require('events');
|
||||
const assert = require('assert');
|
||||
|
||||
class SessionTracker extends Emitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.sessions = new Map();
|
||||
}
|
||||
|
||||
get logger() {
|
||||
if (!this._logger) {
|
||||
const {logger} = require('../../app');
|
||||
this._logger = logger;
|
||||
}
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
add(callSid, callSession) {
|
||||
assert(callSid);
|
||||
this.sessions.set(callSid, callSession);
|
||||
this.logger.info(`SessionTracker:add callSid ${callSid}, we have ${this.sessions.size} session being tracked`);
|
||||
}
|
||||
|
||||
remove(callSid) {
|
||||
assert(callSid);
|
||||
this.sessions.delete(callSid);
|
||||
this.logger.info(`SessionTracker:remove callSid ${callSid}, we have ${this.sessions.size} being tracked`);
|
||||
}
|
||||
|
||||
has(callSid) {
|
||||
return this.sessions.has(callSid);
|
||||
}
|
||||
|
||||
get(callSid) {
|
||||
return this.sessions.get(callSid);
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new SessionTracker();
|
||||
|
||||
module.exports = singleton;
|
||||
@@ -56,10 +56,10 @@ class TaskDial extends Task {
|
||||
this.confirmMethod = this.data.confirmMethod;
|
||||
|
||||
if (this.data.listen) {
|
||||
this.listenTask = makeTask(logger, {'listen': this.data.listen});
|
||||
this.listenTask = makeTask(logger, {'listen': this.data.listen}, this);
|
||||
}
|
||||
if (this.data.transcribe) {
|
||||
this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe});
|
||||
this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe}, this);
|
||||
}
|
||||
|
||||
this.results = {};
|
||||
@@ -131,7 +131,7 @@ class TaskDial extends Task {
|
||||
|
||||
const sbcAddress = cs.direction === CallDirection.Inbound ?
|
||||
`${req.source_address}:${req.source_port}` :
|
||||
config.get('sbcAddress');
|
||||
config.get('outdials.sbc');
|
||||
const opts = {
|
||||
headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers,
|
||||
proxy: `sip:${sbcAddress}`,
|
||||
@@ -268,8 +268,8 @@ class TaskDial extends Task {
|
||||
dialCallSid: sd.callSid,
|
||||
});
|
||||
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep, this);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.ep, this);
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.ep);
|
||||
}
|
||||
|
||||
_bridgeEarlyMedia(sd) {
|
||||
|
||||
@@ -16,10 +16,11 @@ class TaskListen extends Task {
|
||||
this.mixType = this.mixType || 'mono';
|
||||
this.sampleRate = this.sampleRate || 8000;
|
||||
this.earlyMedia = this.data.earlyMedia === true;
|
||||
this.hook = this.normalizeUrl(this.url, 'GET', this.wsAuth);
|
||||
this.nested = typeof parentTask !== 'undefined';
|
||||
this.parentTask = parentTask;
|
||||
this.nested = parentTask instanceof Task;
|
||||
|
||||
this.results = {};
|
||||
this.ranToCompletion = false;
|
||||
|
||||
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
|
||||
|
||||
@@ -31,15 +32,18 @@ class TaskListen extends Task {
|
||||
async exec(cs, ep) {
|
||||
super.exec(cs);
|
||||
this.ep = ep;
|
||||
|
||||
try {
|
||||
this.hook = this.normalizeUrl(this.url, 'GET', this.wsAuth);
|
||||
if (this.playBeep) await this._playBeep(ep);
|
||||
if (this.transcribeTask) {
|
||||
this.logger.debug('TaskListen:exec - starting nested transcribe task');
|
||||
this.transcribeTask.exec(cs, ep, this);
|
||||
this.transcribeTask.exec(cs, ep);
|
||||
}
|
||||
await this._startListening(cs, ep);
|
||||
await this.awaitTaskDone();
|
||||
if (this.action) await this.performAction(this.method, this.auth, this.results, !this.nested);
|
||||
const acceptNewApp = !this.nested && this.ranToCompletion;
|
||||
if (this.action) await this.performAction(this.method, this.auth, this.results, acceptNewApp);
|
||||
} catch (err) {
|
||||
this.logger.info(err, `TaskListen:exec - error ${this.url}`);
|
||||
}
|
||||
@@ -73,9 +77,8 @@ class TaskListen extends Task {
|
||||
this._initListeners(ep);
|
||||
const metadata = Object.assign(
|
||||
{sampleRate: this.sampleRate, mixType: this.mixType},
|
||||
cs.callInfo.toJSON(),
|
||||
this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON(),
|
||||
this.metadata);
|
||||
this.logger.debug({metadata, hook: this.hook}, 'TaskListen:_startListening');
|
||||
if (this.hook.username && this.hook.password) {
|
||||
this.logger.debug({username: this.hook.username, password: this.hook.password},
|
||||
'TaskListen:_startListening basic auth');
|
||||
@@ -94,6 +97,7 @@ class TaskListen extends Task {
|
||||
if (this.maxLength) {
|
||||
this._timer = setTimeout(() => {
|
||||
this.logger.debug(`TaskListen terminating task due to timeout of ${this.timeout}s reached`);
|
||||
this.ranToCompletion = true;
|
||||
this.kill();
|
||||
}, this.maxLength * 1000);
|
||||
}
|
||||
@@ -121,6 +125,7 @@ class TaskListen extends Task {
|
||||
if (evt.dtmf === this.finishOnKey) {
|
||||
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
||||
this.results.digits = evt.dtmf;
|
||||
this.ranToCompletion = true;
|
||||
this.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const Task = require('./task');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const errBadInstruction = new Error('malformed jambonz application payload');
|
||||
|
||||
function makeTask(logger, obj) {
|
||||
function makeTask(logger, obj, parent) {
|
||||
const keys = Object.keys(obj);
|
||||
if (!keys || keys.length !== 1) {
|
||||
throw errBadInstruction;
|
||||
@@ -17,40 +17,40 @@ function makeTask(logger, obj) {
|
||||
switch (name) {
|
||||
case TaskName.SipDecline:
|
||||
const TaskSipDecline = require('./sip_decline');
|
||||
return new TaskSipDecline(logger, data);
|
||||
return new TaskSipDecline(logger, data, parent);
|
||||
case TaskName.Dial:
|
||||
const TaskDial = require('./dial');
|
||||
return new TaskDial(logger, data);
|
||||
return new TaskDial(logger, data, parent);
|
||||
case TaskName.Hangup:
|
||||
const TaskHangup = require('./hangup');
|
||||
return new TaskHangup(logger, data);
|
||||
return new TaskHangup(logger, data, parent);
|
||||
case TaskName.Say:
|
||||
const TaskSay = require('./say');
|
||||
return new TaskSay(logger, data);
|
||||
return new TaskSay(logger, data, parent);
|
||||
case TaskName.Play:
|
||||
const TaskPlay = require('./play');
|
||||
return new TaskPlay(logger, data);
|
||||
return new TaskPlay(logger, data, parent);
|
||||
case TaskName.Pause:
|
||||
const TaskPause = require('./pause');
|
||||
return new TaskPause(logger, data);
|
||||
return new TaskPause(logger, data, parent);
|
||||
case TaskName.Gather:
|
||||
const TaskGather = require('./gather');
|
||||
return new TaskGather(logger, data);
|
||||
return new TaskGather(logger, data, parent);
|
||||
case TaskName.Transcribe:
|
||||
const TaskTranscribe = require('./transcribe');
|
||||
return new TaskTranscribe(logger, data);
|
||||
return new TaskTranscribe(logger, data, parent);
|
||||
case TaskName.Listen:
|
||||
const TaskListen = require('./listen');
|
||||
return new TaskListen(logger, data);
|
||||
return new TaskListen(logger, data, parent);
|
||||
case TaskName.Redirect:
|
||||
const TaskRedirect = require('./redirect');
|
||||
return new TaskRedirect(logger, data);
|
||||
return new TaskRedirect(logger, data, parent);
|
||||
case TaskName.RestDial:
|
||||
const TaskRestDial = require('./rest_dial');
|
||||
return new TaskRestDial(logger, data);
|
||||
return new TaskRestDial(logger, data, parent);
|
||||
case TaskName.Tag:
|
||||
const TaskTag = require('./tag');
|
||||
return new TaskTag(logger, data);
|
||||
return new TaskTag(logger, data, parent);
|
||||
}
|
||||
|
||||
// should never reach
|
||||
|
||||
@@ -57,26 +57,14 @@ class Task extends Emitter {
|
||||
}
|
||||
|
||||
normalizeUrl(url, method, auth) {
|
||||
const hook = {url, method};
|
||||
if (auth && auth.username && auth.password) Object.assign(hook, auth);
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
const or = this.callSession.originalRequest;
|
||||
if (or) {
|
||||
hook.url = `${or.baseUrl}${url}`;
|
||||
hook.method = hook.method || or.method || 'POST';
|
||||
if (!hook.auth && or.auth) Object.assign(hook, or.auth);
|
||||
}
|
||||
}
|
||||
this.logger.debug({hook}, 'Task:normalizeUrl');
|
||||
return hook;
|
||||
return this.callSession.normalizeUrl(url, method, auth);
|
||||
}
|
||||
|
||||
async performAction(method, auth, results, expectResponse = true) {
|
||||
if (this.action) {
|
||||
const hook = this.normalizeUrl(this.action, method, auth);
|
||||
const tasks = await this.actionHook(hook, results, expectResponse);
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
if (expectResponse && tasks && Array.isArray(tasks)) {
|
||||
this.logger.debug({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.callSession.replaceApplication(tasks);
|
||||
}
|
||||
|
||||
31
lib/utils/install-srf-locals.js
Normal file
31
lib/utils/install-srf-locals.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const config = require('config');
|
||||
const ip = require('ip');
|
||||
const localIp = ip.address();
|
||||
const PORT = process.env.HTTP_PORT || config.get('defaultHttpPort');
|
||||
|
||||
function installSrfLocals(srf, logger) {
|
||||
if (srf.locals.dbHelpers) return;
|
||||
|
||||
const {lookupAppByPhoneNumber} = require('jambonz-db-helpers')(config.get('mysql'), logger);
|
||||
const {
|
||||
updateCallStatus,
|
||||
retrieveCall,
|
||||
listCalls,
|
||||
deleteCall
|
||||
} = require('jambonz-realtimedb-helpers')(config.get('redis'), logger);
|
||||
|
||||
Object.assign(srf.locals, {
|
||||
dbHelpers: {
|
||||
lookupAppByPhoneNumber,
|
||||
updateCallStatus,
|
||||
retrieveCall,
|
||||
listCalls,
|
||||
deleteCall
|
||||
},
|
||||
parentLogger: logger,
|
||||
ipv4: localIp,
|
||||
serviceUrl: `http://${localIp}:${PORT}`
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = installSrfLocals;
|
||||
@@ -25,7 +25,7 @@ function normalizeJambones(logger, obj) {
|
||||
throw new Error('malformed jambonz payload: missing verb property');
|
||||
}
|
||||
}
|
||||
logger.debug(`returning document with ${document.length} tasks`);
|
||||
logger.debug({document}, `normalizeJambones: returning document with ${document.length} tasks`);
|
||||
return document;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
|
||||
try {
|
||||
this.updateCallStatus = srf.locals.dbHelpers.updateCallStatus;
|
||||
this.serviceUrl = srf.locals.serviceUrl;
|
||||
|
||||
this.ep = await ms.createEndpoint();
|
||||
this.logger.debug(`SingleDialer:exec - created endpoint ${this.ep.uuid}`);
|
||||
let sdp;
|
||||
@@ -237,6 +240,9 @@ class SingleDialer extends Emitter {
|
||||
} catch (err) {
|
||||
this.logger.info(err, `SingleDialer:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
// update calls db
|
||||
this.updateCallStatus(this.callInfo, this.serviceUrl).catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
package-lock.json
generated
47
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -316,6 +316,11 @@
|
||||
"tape": ">=2.0.0 <5.0.0"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
@@ -797,6 +802,11 @@
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"double-ended-queue": {
|
||||
"version": "2.1.0-0",
|
||||
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
|
||||
"integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
|
||||
},
|
||||
"drachtio-fn-b2b-sugar": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fn-b2b-sugar/-/drachtio-fn-b2b-sugar-0.0.12.tgz",
|
||||
@@ -1718,6 +1728,11 @@
|
||||
"through": "^2.3.6"
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
|
||||
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
|
||||
@@ -2055,6 +2070,16 @@
|
||||
"mysql2": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"jambonz-realtimedb-helpers": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jambonz-realtimedb-helpers/-/jambonz-realtimedb-helpers-0.1.3.tgz",
|
||||
"integrity": "sha512-/lDhucxeR1h9wYvZ+P/UxjfzwTVxgD9IKtZWAJrBleYoLiK0MgTR2gdBThPZv7wbjU0apNcWen06Lf5nccnxQw==",
|
||||
"requires": {
|
||||
"bluebird": "^3.7.2",
|
||||
"debug": "^4.1.1",
|
||||
"redis": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -3017,6 +3042,26 @@
|
||||
"esprima": "~4.0.0"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
|
||||
"integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
|
||||
"requires": {
|
||||
"double-ended-queue": "^2.1.0-0",
|
||||
"redis-commands": "^1.2.0",
|
||||
"redis-parser": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"redis-commands": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz",
|
||||
"integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg=="
|
||||
},
|
||||
"redis-parser": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
|
||||
"integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.3",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -32,7 +32,9 @@
|
||||
"drachtio-fsmrf": "^1.5.12",
|
||||
"drachtio-srf": "^4.4.27",
|
||||
"express": "^4.17.1",
|
||||
"ip": "^1.1.5",
|
||||
"jambonz-db-helpers": "^0.2.0",
|
||||
"jambonz-realtimedb-helpers": "0.1.3",
|
||||
"moment": "^2.24.0",
|
||||
"parse-url": "^5.0.1",
|
||||
"pino": "^5.14.0",
|
||||
|
||||
Reference in New Issue
Block a user