mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-12 09:19:34 +00:00
Add schema validation to create-call req.body, validate app_json via verb-specifications (#488)
* Add schema for create-call, validate app_json via verb-specifications * trigger new build --------- Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# jambones-feature-server 
|
# jambonz-feature-server 
|
||||||
|
|
||||||
This application implements the core feature server of the jambones platform.
|
This application implements the core feature server of the jambones platform.
|
||||||
|
|
||||||
|
|||||||
@@ -5,303 +5,323 @@ const CallInfo = require('../../session/call-info');
|
|||||||
const {CallDirection, CallStatus} = require('../../utils/constants');
|
const {CallDirection, CallStatus} = require('../../utils/constants');
|
||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
const SipError = require('drachtio-srf').SipError;
|
const SipError = require('drachtio-srf').SipError;
|
||||||
|
const { validationResult } = require('express-validator');
|
||||||
|
const { validate } = require('@jambonz/verb-specifications');
|
||||||
const sysError = require('./error');
|
const sysError = require('./error');
|
||||||
const HttpRequestor = require('../../utils/http-requestor');
|
const HttpRequestor = require('../../utils/http-requestor');
|
||||||
const WsRequestor = require('../../utils/ws-requestor');
|
const WsRequestor = require('../../utils/ws-requestor');
|
||||||
const RootSpan = require('../../utils/call-tracer');
|
const RootSpan = require('../../utils/call-tracer');
|
||||||
const dbUtils = require('../../utils/db-utils');
|
const dbUtils = require('../../utils/db-utils');
|
||||||
const { mergeSdpMedia, extractSdpMedia } = require('../../utils/sdp-utils');
|
const { mergeSdpMedia, extractSdpMedia } = require('../../utils/sdp-utils');
|
||||||
|
const { createCallSchema } = require('../schemas/create-call');
|
||||||
|
|
||||||
router.post('/', async(req, res) => {
|
router.post('/',
|
||||||
const {logger} = req.app.locals;
|
createCallSchema,
|
||||||
const accountSid = req.body.account_sid;
|
async(req, res) => {
|
||||||
const {srf} = require('../../..');
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
logger.debug({body: req.body}, 'got createCall request');
|
return res.status(400).json({ errors: errors.array() });
|
||||||
try {
|
|
||||||
let uri, cs, to;
|
|
||||||
// app_json is creaeted by only api-server.
|
|
||||||
// if it available, take it and delete before creating task
|
|
||||||
const app_json = req.body.app_json;
|
|
||||||
delete req.body.app_json;
|
|
||||||
const restDial = makeTask(logger, {'rest:dial': req.body});
|
|
||||||
restDial.appJson = app_json;
|
|
||||||
const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
|
|
||||||
const {
|
|
||||||
lookupAppBySid
|
|
||||||
} = srf.locals.dbHelpers;
|
|
||||||
const {getSBC, getFreeswitch} = srf.locals;
|
|
||||||
const sbcAddress = getSBC();
|
|
||||||
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
|
||||||
const target = restDial.to;
|
|
||||||
const opts = {
|
|
||||||
callingNumber: restDial.from,
|
|
||||||
...(restDial.callerName && {callingName: restDial.callerName}),
|
|
||||||
headers: req.body.headers || {}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
|
|
||||||
const account = await lookupAccountBySid(req.body.account_sid);
|
|
||||||
const accountInfo = await lookupAccountDetails(req.body.account_sid);
|
|
||||||
const callSid = uuidv4();
|
|
||||||
const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null;
|
|
||||||
const record_all_calls = account.record_all_calls || (application && application.record_all_calls);
|
|
||||||
const recordOutputFormat = account.record_format || 'mp3';
|
|
||||||
const rootSpan = new RootSpan('rest-call', {
|
|
||||||
callSid,
|
|
||||||
accountSid,
|
|
||||||
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid})
|
|
||||||
});
|
|
||||||
|
|
||||||
opts.headers = {
|
|
||||||
...opts.headers,
|
|
||||||
'X-Jambonz-Routing': target.type,
|
|
||||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
|
||||||
'X-Call-Sid': callSid,
|
|
||||||
'X-Account-Sid': accountSid,
|
|
||||||
'X-Trace-ID': rootSpan.traceId,
|
|
||||||
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
|
||||||
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
|
||||||
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat})
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (target.type) {
|
|
||||||
case 'phone':
|
|
||||||
case 'teams':
|
|
||||||
uri = `sip:${target.number}@${sbcAddress}`;
|
|
||||||
to = target.number;
|
|
||||||
if ('teams' === target.type) {
|
|
||||||
const obj = await lookupTeamsByAccount(accountSid);
|
|
||||||
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
|
|
||||||
Object.assign(opts.headers, {
|
|
||||||
'X-MS-Teams-FQDN': obj.ms_teams_fqdn,
|
|
||||||
'X-MS-Teams-Tenant-FQDN': target.tenant || obj.tenant_fqdn
|
|
||||||
});
|
|
||||||
if (target.vmail === true) uri = `${uri};opaque=app:voicemail`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'user':
|
|
||||||
uri = `sip:${target.name}`;
|
|
||||||
to = target.name;
|
|
||||||
if (target.overrideTo) {
|
|
||||||
Object.assign(opts.headers, {
|
|
||||||
'X-Override-To': target.overrideTo
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'sip':
|
|
||||||
uri = target.sipUri;
|
|
||||||
to = uri;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
const {logger} = req.app.locals;
|
||||||
|
const accountSid = req.body.account_sid;
|
||||||
|
const {srf} = require('../../..');
|
||||||
|
|
||||||
if (target.type === 'phone' && target.trunk) {
|
const app_json = req.body['app_json'];
|
||||||
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
try {
|
||||||
logger.info(
|
// app_json is created only by api-server.
|
||||||
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
if (app_json) {
|
||||||
if (voip_carrier_sid) {
|
// if available, delete from req before creating task
|
||||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
delete req.body.app_json;
|
||||||
|
// validate possible app_json via verb-specifications
|
||||||
|
validate(logger, JSON.parse(app_json));
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug({ err }, `invalid app_json: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
logger.debug({body: req.body}, 'got createCall request');
|
||||||
|
try {
|
||||||
|
let uri, cs, to;
|
||||||
|
|
||||||
|
const restDial = makeTask(logger, { 'rest:dial': req.body });
|
||||||
|
restDial.appJson = app_json;
|
||||||
|
|
||||||
|
const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
|
||||||
|
const {
|
||||||
|
lookupAppBySid
|
||||||
|
} = srf.locals.dbHelpers;
|
||||||
|
const {getSBC, getFreeswitch} = srf.locals;
|
||||||
|
const sbcAddress = getSBC();
|
||||||
|
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
||||||
|
const target = restDial.to;
|
||||||
|
const opts = {
|
||||||
|
callingNumber: restDial.from,
|
||||||
|
...(restDial.callerName && {callingName: restDial.callerName}),
|
||||||
|
headers: req.body.headers || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
|
||||||
|
const account = await lookupAccountBySid(req.body.account_sid);
|
||||||
|
const accountInfo = await lookupAccountDetails(req.body.account_sid);
|
||||||
|
const callSid = uuidv4();
|
||||||
|
const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null;
|
||||||
|
const record_all_calls = account.record_all_calls || (application && application.record_all_calls);
|
||||||
|
const recordOutputFormat = account.record_format || 'mp3';
|
||||||
|
const rootSpan = new RootSpan('rest-call', {
|
||||||
|
callSid,
|
||||||
|
accountSid,
|
||||||
|
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid})
|
||||||
|
});
|
||||||
|
|
||||||
|
opts.headers = {
|
||||||
|
...opts.headers,
|
||||||
|
'X-Jambonz-Routing': target.type,
|
||||||
|
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||||
|
'X-Call-Sid': callSid,
|
||||||
|
'X-Account-Sid': accountSid,
|
||||||
|
'X-Trace-ID': rootSpan.traceId,
|
||||||
|
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
||||||
|
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
||||||
|
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat})
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (target.type) {
|
||||||
|
case 'phone':
|
||||||
|
case 'teams':
|
||||||
|
uri = `sip:${target.number}@${sbcAddress}`;
|
||||||
|
to = target.number;
|
||||||
|
if ('teams' === target.type) {
|
||||||
|
const obj = await lookupTeamsByAccount(accountSid);
|
||||||
|
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
|
||||||
|
Object.assign(opts.headers, {
|
||||||
|
'X-MS-Teams-FQDN': obj.ms_teams_fqdn,
|
||||||
|
'X-MS-Teams-Tenant-FQDN': target.tenant || obj.tenant_fqdn
|
||||||
|
});
|
||||||
|
if (target.vmail === true) uri = `${uri};opaque=app:voicemail`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'user':
|
||||||
|
uri = `sip:${target.name}`;
|
||||||
|
to = target.name;
|
||||||
|
if (target.overrideTo) {
|
||||||
|
Object.assign(opts.headers, {
|
||||||
|
'X-Override-To': target.overrideTo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'sip':
|
||||||
|
uri = target.sipUri;
|
||||||
|
to = uri;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.type === 'phone' && target.trunk) {
|
||||||
|
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
||||||
|
logger.info(
|
||||||
|
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
||||||
|
if (voip_carrier_sid) {
|
||||||
|
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* trunk isn't specified,
|
* trunk isn't specified,
|
||||||
* check if from-number matches any existing numbers on Jambonz
|
* check if from-number matches any existing numbers on Jambonz
|
||||||
* */
|
* */
|
||||||
if (target.type === 'phone' && !target.trunk) {
|
if (target.type === 'phone' && !target.trunk) {
|
||||||
const str = restDial.from || '';
|
const str = restDial.from || '';
|
||||||
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
|
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
|
||||||
logger.info(
|
logger.info(
|
||||||
`createCall: selected ${voip_carrier_sid} for requested phone number: ${callingNumber || 'unspecified'})`);
|
`createCall: selected ${voip_carrier_sid} for requested phone number: ${callingNumber || 'unspecified'})`);
|
||||||
if (voip_carrier_sid) {
|
if (voip_carrier_sid) {
|
||||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* create endpoint for outdial */
|
|
||||||
const ms = getFreeswitch();
|
|
||||||
if (!ms) throw new Error('no available Freeswitch for outbound call creation');
|
|
||||||
const ep = await ms.createEndpoint();
|
|
||||||
logger.debug(`createCall: successfully allocated endpoint, sending INVITE to ${sbcAddress}`);
|
|
||||||
|
|
||||||
/* launch outdial */
|
|
||||||
let sdp, sipLogger;
|
|
||||||
let dualEp;
|
|
||||||
let localSdp = ep.local.sdp;
|
|
||||||
|
|
||||||
if (req.body.dual_streams) {
|
|
||||||
dualEp = await ms.createEndpoint();
|
|
||||||
localSdp = mergeSdpMedia(localSdp, dualEp.local.sdp);
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectStream = async(remoteSdp) => {
|
|
||||||
if (remoteSdp !== sdp) {
|
|
||||||
sdp = remoteSdp;
|
|
||||||
if (req.body.dual_streams) {
|
|
||||||
const [sdpLegA, sdpLebB] = extractSdpMedia(remoteSdp);
|
|
||||||
|
|
||||||
await ep.modify(sdpLegA);
|
|
||||||
await dualEp.modify(sdpLebB);
|
|
||||||
await ep.bridge(dualEp);
|
|
||||||
} else {
|
|
||||||
ep.modify(sdp);
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
/* create endpoint for outdial */
|
||||||
Object.assign(opts, {
|
const ms = getFreeswitch();
|
||||||
proxy: `sip:${sbcAddress}`,
|
if (!ms) throw new Error('no available Freeswitch for outbound call creation');
|
||||||
localSdp
|
const ep = await ms.createEndpoint();
|
||||||
});
|
logger.debug(`createCall: successfully allocated endpoint, sending INVITE to ${sbcAddress}`);
|
||||||
if (target.auth) opts.auth = target.auth;
|
|
||||||
|
/* launch outdial */
|
||||||
|
let sdp, sipLogger;
|
||||||
|
let dualEp;
|
||||||
|
let localSdp = ep.local.sdp;
|
||||||
|
|
||||||
|
if (req.body.dual_streams) {
|
||||||
|
dualEp = await ms.createEndpoint();
|
||||||
|
localSdp = mergeSdpMedia(localSdp, dualEp.local.sdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectStream = async(remoteSdp) => {
|
||||||
|
if (remoteSdp !== sdp) {
|
||||||
|
sdp = remoteSdp;
|
||||||
|
if (req.body.dual_streams) {
|
||||||
|
const [sdpLegA, sdpLebB] = extractSdpMedia(remoteSdp);
|
||||||
|
|
||||||
|
await ep.modify(sdpLegA);
|
||||||
|
await dualEp.modify(sdpLebB);
|
||||||
|
await ep.bridge(dualEp);
|
||||||
|
} else {
|
||||||
|
ep.modify(sdp);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
Object.assign(opts, {
|
||||||
|
proxy: `sip:${sbcAddress}`,
|
||||||
|
localSdp
|
||||||
|
});
|
||||||
|
if (target.auth) opts.auth = target.auth;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create our application object -
|
* create our application object -
|
||||||
* not from the database as per an inbound call,
|
* not from the database as per an inbound call,
|
||||||
* but from the provided params in the request
|
* but from the provided params in the request
|
||||||
*/
|
*/
|
||||||
const app = req.body;
|
const app = req.body;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* attach our requestor and notifier objects
|
* attach our requestor and notifier objects
|
||||||
* these will be used for all http requests we make during this call
|
* these will be used for all http requests we make during this call
|
||||||
*/
|
*/
|
||||||
if ('WS' === app.call_hook?.method || /^wss?:/.test(app.call_hook.url)) {
|
if ('WS' === app.call_hook?.method || /^wss?:/.test(app.call_hook.url)) {
|
||||||
logger.debug({call_hook: app.call_hook}, 'creating websocket for call hook');
|
logger.debug({call_hook: app.call_hook}, 'creating websocket for call hook');
|
||||||
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
||||||
if (app.call_hook.url === app.call_status_hook.url || !app.call_status_hook?.url) {
|
if (app.call_hook.url === app.call_status_hook.url || !app.call_status_hook?.url) {
|
||||||
logger.debug('reusing websocket for call status hook');
|
logger.debug('reusing websocket for call status hook');
|
||||||
app.notifier = app.requestor;
|
app.notifier = app.requestor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook');
|
||||||
|
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||||
|
}
|
||||||
|
if (!app.notifier && app.call_status_hook) {
|
||||||
|
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
||||||
|
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
|
||||||
|
}
|
||||||
|
else if (!app.notifier) {
|
||||||
|
logger.debug('creating null call status hook');
|
||||||
|
app.notifier = {request: () => {}, close: () => {}};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook');
|
|
||||||
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
|
||||||
}
|
|
||||||
if (!app.notifier && app.call_status_hook) {
|
|
||||||
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
|
||||||
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
|
|
||||||
}
|
|
||||||
else if (!app.notifier) {
|
|
||||||
logger.debug('creating null call status hook');
|
|
||||||
app.notifier = {request: () => {}, close: () => {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* now launch the outdial */
|
/* now launch the outdial */
|
||||||
try {
|
try {
|
||||||
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
||||||
cbRequest: (err, inviteReq) => {
|
cbRequest: (err, inviteReq) => {
|
||||||
/* in case of 302 redirect, this gets called twice, ignore the second
|
/* in case of 302 redirect, this gets called twice, ignore the second
|
||||||
except to update the req so that it can later be canceled if need be
|
except to update the req so that it can later be canceled if need be
|
||||||
*/
|
*/
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
logger.info(`create-call: got redirect, updating request to new call-id ${req.get('Call-ID')}`);
|
logger.info(`create-call: got redirect, updating request to new call-id ${req.get('Call-ID')}`);
|
||||||
if (cs) cs.req = inviteReq;
|
if (cs) cs.req = inviteReq;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
logger.error(err, 'createCall Error creating call');
|
||||||
|
res.status(500).send('Call Failure');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inviteReq.srf = srf;
|
||||||
|
inviteReq.locals = {
|
||||||
|
...(inviteReq || {}),
|
||||||
|
callSid,
|
||||||
|
application_sid: app.application_sid
|
||||||
|
};
|
||||||
|
/* ok our outbound INVITE is in flight */
|
||||||
|
|
||||||
|
const tasks = [restDial];
|
||||||
|
sipLogger = logger.child({
|
||||||
|
callSid,
|
||||||
|
callId: inviteReq.get('Call-ID'),
|
||||||
|
accountSid,
|
||||||
|
traceId: rootSpan.traceId
|
||||||
|
});
|
||||||
|
app.requestor.logger = app.notifier.logger = sipLogger;
|
||||||
|
const callInfo = new CallInfo({
|
||||||
|
direction: CallDirection.Outbound,
|
||||||
|
req: inviteReq,
|
||||||
|
to,
|
||||||
|
tag: app.tag,
|
||||||
|
callSid,
|
||||||
|
accountSid: req.body.account_sid,
|
||||||
|
applicationSid: app.application_sid,
|
||||||
|
traceId: rootSpan.traceId
|
||||||
|
});
|
||||||
|
cs = new RestCallSession({
|
||||||
|
logger: sipLogger,
|
||||||
|
application: app,
|
||||||
|
srf,
|
||||||
|
req: inviteReq,
|
||||||
|
ep,
|
||||||
|
ep2: dualEp,
|
||||||
|
tasks,
|
||||||
|
callInfo,
|
||||||
|
accountInfo,
|
||||||
|
rootSpan
|
||||||
|
});
|
||||||
|
cs.exec(req);
|
||||||
|
|
||||||
|
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')});
|
||||||
|
|
||||||
|
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')},
|
||||||
|
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
|
||||||
|
},
|
||||||
|
cbProvisional: (prov) => {
|
||||||
|
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
|
||||||
|
if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body);
|
||||||
|
restDial.emit('callStatus', prov.status, !!prov.body);
|
||||||
|
cs.emit('callStatusChange', {callStatus, sipStatus: prov.status});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if (err) {
|
connectStream(dlg.remote.sdp);
|
||||||
logger.error(err, 'createCall Error creating call');
|
cs.emit('callStatusChange', {
|
||||||
res.status(500).send('Call Failure');
|
callStatus: CallStatus.InProgress,
|
||||||
return;
|
sipStatus: 200,
|
||||||
}
|
sipReason: 'OK'
|
||||||
inviteReq.srf = srf;
|
});
|
||||||
inviteReq.locals = {
|
restDial.emit('callStatus', 200);
|
||||||
...(inviteReq || {}),
|
restDial.emit('connect', dlg);
|
||||||
callSid,
|
}
|
||||||
application_sid: app.application_sid
|
catch (err) {
|
||||||
};
|
let callStatus = CallStatus.Failed;
|
||||||
/* ok our outbound INVITE is in flight */
|
if (err instanceof SipError) {
|
||||||
|
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
|
||||||
const tasks = [restDial];
|
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
|
||||||
sipLogger = logger.child({
|
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
|
||||||
callSid,
|
else console.log(`REST outdial failed with ${err.status}`);
|
||||||
callId: inviteReq.get('Call-ID'),
|
if (cs) cs.emit('callStatusChange', {
|
||||||
accountSid,
|
callStatus,
|
||||||
traceId: rootSpan.traceId
|
sipStatus: err.status,
|
||||||
|
sipReason: err.reason
|
||||||
});
|
});
|
||||||
app.requestor.logger = app.notifier.logger = sipLogger;
|
cs.callGone = true;
|
||||||
const callInfo = new CallInfo({
|
|
||||||
direction: CallDirection.Outbound,
|
|
||||||
req: inviteReq,
|
|
||||||
to,
|
|
||||||
tag: app.tag,
|
|
||||||
callSid,
|
|
||||||
accountSid: req.body.account_sid,
|
|
||||||
applicationSid: app.application_sid,
|
|
||||||
traceId: rootSpan.traceId
|
|
||||||
});
|
|
||||||
cs = new RestCallSession({
|
|
||||||
logger: sipLogger,
|
|
||||||
application: app,
|
|
||||||
srf,
|
|
||||||
req: inviteReq,
|
|
||||||
ep,
|
|
||||||
ep2: dualEp,
|
|
||||||
tasks,
|
|
||||||
callInfo,
|
|
||||||
accountInfo,
|
|
||||||
rootSpan
|
|
||||||
});
|
|
||||||
cs.exec(req);
|
|
||||||
|
|
||||||
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')});
|
|
||||||
|
|
||||||
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')},
|
|
||||||
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
|
|
||||||
},
|
|
||||||
cbProvisional: (prov) => {
|
|
||||||
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
|
|
||||||
if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body);
|
|
||||||
restDial.emit('callStatus', prov.status, !!prov.body);
|
|
||||||
cs.emit('callStatusChange', {callStatus, sipStatus: prov.status});
|
|
||||||
}
|
}
|
||||||
});
|
else {
|
||||||
connectStream(dlg.remote.sdp);
|
if (cs) cs.emit('callStatusChange', {
|
||||||
cs.emit('callStatusChange', {
|
callStatus,
|
||||||
callStatus: CallStatus.InProgress,
|
sipStatus: 500,
|
||||||
sipStatus: 200,
|
sipReason: 'Internal Server Error'
|
||||||
sipReason: 'OK'
|
});
|
||||||
});
|
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
|
||||||
restDial.emit('callStatus', 200);
|
else console.error(err);
|
||||||
restDial.emit('connect', dlg);
|
}
|
||||||
|
ep.destroy();
|
||||||
|
if (dualEp) {
|
||||||
|
dualEp.destroy();
|
||||||
|
}
|
||||||
|
setTimeout(restDial.kill.bind(restDial, cs), 5000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
sysError(logger, res, err);
|
||||||
}
|
}
|
||||||
catch (err) {
|
});
|
||||||
let callStatus = CallStatus.Failed;
|
|
||||||
if (err instanceof SipError) {
|
|
||||||
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
|
|
||||||
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
|
|
||||||
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
|
|
||||||
else console.log(`REST outdial failed with ${err.status}`);
|
|
||||||
if (cs) cs.emit('callStatusChange', {
|
|
||||||
callStatus,
|
|
||||||
sipStatus: err.status,
|
|
||||||
sipReason: err.reason
|
|
||||||
});
|
|
||||||
cs.callGone = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (cs) cs.emit('callStatusChange', {
|
|
||||||
callStatus,
|
|
||||||
sipStatus: 500,
|
|
||||||
sipReason: 'Internal Server Error'
|
|
||||||
});
|
|
||||||
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
|
|
||||||
else console.error(err);
|
|
||||||
}
|
|
||||||
ep.destroy();
|
|
||||||
if (dualEp) {
|
|
||||||
dualEp.destroy();
|
|
||||||
}
|
|
||||||
setTimeout(restDial.kill.bind(restDial, cs), 5000);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
sysError(logger, res, err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
114
lib/http-routes/schemas/create-call.js
Normal file
114
lib/http-routes/schemas/create-call.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
const { checkSchema } = require('express-validator');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @path api-server {{base_url}}/v1/Accounts/:account_sid/Calls
|
||||||
|
* @see https://api.jambonz.org/#243a2edd-7999-41db-bd0d-08082bbab401
|
||||||
|
*/
|
||||||
|
const createCallSchema = checkSchema({
|
||||||
|
application_sid: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
isLength: { options: { min: 36, max: 36 } },
|
||||||
|
errorMessage: 'Invalid application_sid',
|
||||||
|
},
|
||||||
|
answerOnBridge: {
|
||||||
|
isBoolean: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid answerOnBridge',
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
errorMessage: 'Invalid from',
|
||||||
|
isString: true,
|
||||||
|
isLength: {
|
||||||
|
options: { min: 1, max: 256 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fromHost: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid fromHost',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
errorMessage: 'Invalid to',
|
||||||
|
isObject: true,
|
||||||
|
},
|
||||||
|
callerName: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid callerName',
|
||||||
|
},
|
||||||
|
amd: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid tag',
|
||||||
|
},
|
||||||
|
'tag.*': {
|
||||||
|
trim: true,
|
||||||
|
escape: true,
|
||||||
|
stripLow: true,
|
||||||
|
},
|
||||||
|
app_json: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid app_json',
|
||||||
|
},
|
||||||
|
account_sid: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid account_sid',
|
||||||
|
isLength: { options: { min: 36, max: 36 } },
|
||||||
|
},
|
||||||
|
timeout: {
|
||||||
|
isInt: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid timeout',
|
||||||
|
},
|
||||||
|
timeLimit: {
|
||||||
|
isInt: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid timeLimit',
|
||||||
|
},
|
||||||
|
call_hook: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid call_hook',
|
||||||
|
},
|
||||||
|
call_status_hook: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid call_status_hook',
|
||||||
|
},
|
||||||
|
speech_synthesis_vendor: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_synthesis_vendor',
|
||||||
|
},
|
||||||
|
speech_synthesis_language: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_synthesis_language',
|
||||||
|
},
|
||||||
|
speech_synthesis_voice: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_synthesis_voice',
|
||||||
|
},
|
||||||
|
speech_recognizer_vendor: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_recognizer_vendor',
|
||||||
|
},
|
||||||
|
speech_recognizer_language: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_recognizer_language',
|
||||||
|
}
|
||||||
|
}, ['body']);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createCallSchema
|
||||||
|
};
|
||||||
35
package-lock.json
generated
35
package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"drachtio-fsmrf": "^3.0.27",
|
"drachtio-fsmrf": "^3.0.27",
|
||||||
"drachtio-srf": "^4.5.29",
|
"drachtio-srf": "^4.5.29",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-validator": "^7.0.1",
|
||||||
"ip": "^1.1.8",
|
"ip": "^1.1.8",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"parse-url": "^8.1.0",
|
"parse-url": "^8.1.0",
|
||||||
@@ -5836,6 +5837,18 @@
|
|||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-validator": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"validator": "^13.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/debug": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@@ -10238,6 +10251,14 @@
|
|||||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/validator": {
|
||||||
|
"version": "13.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
|
||||||
|
"integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -15184,6 +15205,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"express-validator": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"validator": "^13.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ext": {
|
"ext": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||||
@@ -18478,6 +18508,11 @@
|
|||||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"validator": {
|
||||||
|
"version": "13.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
|
||||||
|
"integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ=="
|
||||||
|
},
|
||||||
"vary": {
|
"vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"drachtio-fsmrf": "^3.0.27",
|
"drachtio-fsmrf": "^3.0.27",
|
||||||
"drachtio-srf": "^4.5.29",
|
"drachtio-srf": "^4.5.29",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-validator": "^7.0.1",
|
||||||
"ip": "^1.1.8",
|
"ip": "^1.1.8",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"parse-url": "^8.1.0",
|
"parse-url": "^8.1.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user