mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
added initial support for REST-initiated outdials
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"Listen": "listen",
|
||||
"Play": "play",
|
||||
"Redirect": "redirect",
|
||||
"RestDial": "rest:dial",
|
||||
"SipDecline": "sip:decline",
|
||||
"SipNotify": "sip:notify",
|
||||
"SipRedirect": "sip:redirect",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
function normalizeJambones(logger, obj) {
|
||||
logger.debug(`normalizeJambones: ${JSON.stringify(obj)}`);
|
||||
if (!Array.isArray(obj)) throw new Error('invalid JSON: jambones docs must be array');
|
||||
if (!Array.isArray(obj)) throw new Error('malformed jambonz payload: must be array');
|
||||
const document = [];
|
||||
for (const tdata of obj) {
|
||||
if (typeof tdata !== 'object') throw new Error('invalid JSON: jambones docs must be array of objects');
|
||||
if (typeof tdata !== 'object') throw new Error('malformed jambonz payload: must be array of objects');
|
||||
if ('verb' in tdata) {
|
||||
// {verb: 'say', text: 'foo..bar'..}
|
||||
const name = tdata.verb;
|
||||
@@ -21,8 +21,8 @@ function normalizeJambones(logger, obj) {
|
||||
document.push(tdata);
|
||||
}
|
||||
else {
|
||||
logger.info(tdata, `invalid JSON: invalid verb form, numkeys ${Object.keys(tdata).length}`);
|
||||
throw new Error('invalid JSON: invalid verb form');
|
||||
logger.info(tdata, 'malformed jambonz payload: missing verb property');
|
||||
throw new Error('malformed jambonz payload: missing verb property');
|
||||
}
|
||||
}
|
||||
logger.debug(`returning document with ${document.length} tasks`);
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
const request = require('request');
|
||||
require('request-debug')(request);
|
||||
//require('request-debug')(request);
|
||||
const retrieveApp = require('./retrieve-app');
|
||||
|
||||
function hooks(logger, callInfo) {
|
||||
logger.debug({callInfo}, 'creating action hook');
|
||||
function actionHook(hook, obj = {}, expectResponse = true) {
|
||||
const method = hook.method.toUpperCase();
|
||||
const method = (hook.method || 'POST').toUpperCase();
|
||||
const auth = (hook.username && hook.password) ?
|
||||
{username: hook.username, password: hook.password} :
|
||||
null;
|
||||
|
||||
const data = Object.assign({}, obj, callInfo);
|
||||
logger.debug({data}, `actionhook sending to ${hook.url}`);
|
||||
if ('GET' === method) {
|
||||
// remove customer data - only for POSTs since it might be quite complex
|
||||
delete data.customerData;
|
||||
}
|
||||
const data = Object.assign({}, obj, callInfo.toJSON());
|
||||
logger.debug({hook, data, auth}, 'actionhook');
|
||||
|
||||
/* customer data only on POSTs */
|
||||
if ('GET' === method) delete data.customerData;
|
||||
|
||||
const opts = {
|
||||
url: hook.url,
|
||||
method,
|
||||
json: 'POST' === method || expectResponse
|
||||
};
|
||||
if (auth) obj.auth = auth;
|
||||
if (auth) opts.auth = auth;
|
||||
if ('POST' === method) opts.body = data;
|
||||
else opts.qs = data;
|
||||
|
||||
@@ -40,8 +39,8 @@ function hooks(logger, callInfo) {
|
||||
});
|
||||
}
|
||||
|
||||
function notifyHook(url, method, auth, opts = {}) {
|
||||
return actionHook(url, method, auth, opts, false);
|
||||
function notifyHook(hook, opts = {}) {
|
||||
return actionHook(hook, opts, false);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const hooks = require('./notifiers');
|
||||
const moment = require('moment');
|
||||
const parseUrl = require('parse-url');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo}) {
|
||||
@@ -25,23 +26,13 @@ class SingleDialer extends Emitter {
|
||||
this.bindings = logger.bindings();
|
||||
|
||||
this.parentCallInfo = callInfo;
|
||||
/*
|
||||
this.callInfo = Object.assign({}, callInfo, {
|
||||
callSid: this._callSid,
|
||||
parentCallSid: callInfo.callSid,
|
||||
direction: CallDirection.Outbound,
|
||||
callStatus: CallStatus.Trying,
|
||||
sipStatus: 100
|
||||
});
|
||||
*/
|
||||
this.callGone = false;
|
||||
|
||||
this.callSid = uuidv4();
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
}
|
||||
|
||||
get callSid() {
|
||||
return this._callSid;
|
||||
}
|
||||
get callStatus() {
|
||||
return this.callInfo.callStatus;
|
||||
}
|
||||
@@ -88,21 +79,26 @@ class SingleDialer extends Emitter {
|
||||
if (this.target.auth) opts.auth = this.target.auth;
|
||||
this.dlg = await srf.createUAC(uri, opts, {
|
||||
cbRequest: (err, req) => {
|
||||
if (err) return this.logger.error(err, 'SingleDialer:exec Error creating call');
|
||||
if (err) {
|
||||
this.logger.error(err, 'SingleDialer:exec Error creating call');
|
||||
this.emit('callCreateFail', err);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* INVITE has been sent out
|
||||
* (a) create a CallInfo for this call
|
||||
* (a) create a logger for this call
|
||||
* (b) augment this.callInfo with additional call info
|
||||
*/
|
||||
this.logger.debug(`call sent, creating CallInfo parentCallInfo is CallInfo? ${this.parentCallInfo instanceof CallInfo}`);
|
||||
this.callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
parentCallInfo: this.parentCallInfo,
|
||||
req
|
||||
req,
|
||||
to,
|
||||
callSid: this.callSid
|
||||
});
|
||||
this.logger = srf.locals.parentLogger.child({
|
||||
callSid: this.callInfo.callSid,
|
||||
callSid: this.callSid,
|
||||
parentCallSid: this.parentCallInfo.callSid,
|
||||
callId: this.callInfo.callId
|
||||
});
|
||||
@@ -164,6 +160,7 @@ class SingleDialer extends Emitter {
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.logger.debug('SingleDialer:kill hanging up called party');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.dlg.destroy();
|
||||
}
|
||||
if (this.ep) {
|
||||
this.logger.debug(`SingleDialer:kill - deleting endpoint ${this.ep.uuid}`);
|
||||
@@ -181,13 +178,14 @@ class SingleDialer extends Emitter {
|
||||
async _executeApp(url) {
|
||||
this.logger.debug(`SingleDialer:_executeApp: executing ${url} after connect`);
|
||||
try {
|
||||
let auth;
|
||||
let auth, method;
|
||||
const app = Object.assign({}, this.application);
|
||||
if (url.startsWith('/')) {
|
||||
const savedUrl = url;
|
||||
const or = app.originalRequest;
|
||||
url = `${or.baseUrl}${url}`;
|
||||
auth = or.auth;
|
||||
method = this.method || or.method || 'POST';
|
||||
this.logger.debug({originalUrl: savedUrl, normalizedUrl: url}, 'SingleDialer:_executeApp normalized url');
|
||||
}
|
||||
else {
|
||||
@@ -196,9 +194,10 @@ class SingleDialer extends Emitter {
|
||||
app.originalRequest = {
|
||||
baseUrl: `${u.protocol}://${u.resource}${myPort}`
|
||||
};
|
||||
method = this.method || 'POST';
|
||||
}
|
||||
|
||||
const tasks = await this.actionHook(url, this.method, auth);
|
||||
const tasks = await this.actionHook({url, method, auth});
|
||||
const allowedTasks = tasks.filter((task) => {
|
||||
return [
|
||||
TaskPreconditions.StableCall,
|
||||
@@ -210,7 +209,14 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
|
||||
this.logger.debug(`SingleDialer:_executeApp: executing ${tasks.length} tasks`);
|
||||
const cs = new ConfirmCallSession({logger: this.logger, application: app, dlg: this.dlg, ep: this.ep, tasks});
|
||||
const cs = new ConfirmCallSession({
|
||||
logger: this.logger,
|
||||
application: app,
|
||||
dlg: this.dlg,
|
||||
ep: this.ep,
|
||||
callInfo: this.callInfo,
|
||||
tasks
|
||||
});
|
||||
await cs.exec();
|
||||
this.emit(this.dlg.connected ? 'accept' : 'decline');
|
||||
} catch (err) {
|
||||
@@ -220,9 +226,12 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
_notifyCallStatusChange({callStatus, sipStatus}) {
|
||||
this.logger.debug(`SingleDialer:_notifyCallStatusChange: ${callStatus} ${sipStatus}`);
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.notifyHook(this.application.call_status_hook);
|
||||
} catch (err) {
|
||||
|
||||
@@ -12,7 +12,9 @@ function retrieveUrl(logger, url, method, auth, obj) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(opts, (err, response, body) => {
|
||||
if (err) throw err;
|
||||
if (body) logger.debug({body}, 'retrieveUrl: customer returned an application');
|
||||
if (response.statusCode === 401) return reject(new Error('HTTP request failed: Unauthorized'));
|
||||
else if (response.statusCode !== 200) return reject(new Error(`HTTP request failed: ${response.statusCode}`));
|
||||
if (body) logger.debug({body}, 'retrieveUrl: ${method} ${url} returned an application');
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user