initial support for live call control

This commit is contained in:
Dave Horton
2020-02-07 10:18:53 -05:00
parent eddd4a9a27
commit ec02052d27
7 changed files with 280 additions and 25 deletions
+8 -1
View File
@@ -9,11 +9,18 @@ const cors = require('cors');
const passport = require('passport');
const authStrategy = require('./lib/auth')(logger);
const routes = require('./lib/routes');
const {retrieveCall, deleteCall, listCalls} = require('jambonz-realtimedb-helpers')(config.get('redis'), logger);
const PORT = process.env.HTTP_PORT || 3000;
passport.use(authStrategy);
app.locals.logger = logger;
app.locals = app.locals || {};
Object.assign(app.locals, {
logger,
retrieveCall,
deleteCall,
listCalls
});
app.use(cors());
app.use(express.urlencoded({ extended: true }));
+6 -1
View File
@@ -9,7 +9,12 @@
"database": "jambones",
"connectionLimit": 10
},
"redis": {
"host": "127.0.0.1",
"port": 6379
},
"services": {
"createCall": "http://feature.server/createCall:3000"
"apiVersion" : "v1",
"createCall": "http://feature.server/v1/createCall:3000"
}
}
+4
View File
@@ -7,5 +7,9 @@
"user": "jambones_test",
"database": "jambones_test",
"password": "jambones_test"
},
"redis": {
"host": "127.0.0.1",
"port": 6379
}
}
+104 -2
View File
@@ -6,12 +6,14 @@ const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const ServiceProvider = require('../../models/service-provider');
const decorate = require('./decorate');
const snakeCase = require('../../utils/snake-case');
const sysError = require('./error');
const preconditions = {
'add': validateAdd,
'update': validateUpdate,
'delete': validateDelete
};
const API_VERSION = config.get('services.apiVersion');
function validateTo(to) {
if (to && typeof to === 'object') {
@@ -213,6 +215,9 @@ router.put('/:sid', async(req, res) => {
}
});
/**
* create a new Call
*/
router.post('/:sid/Calls', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
@@ -230,7 +235,7 @@ router.post('/:sid/Calls', async(req, res) => {
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.send(500);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
@@ -241,7 +246,104 @@ router.post('/:sid/Calls', async(req, res) => {
} catch (err) {
sysError(logger, res, err);
}
});
/**
* retrieve info for a group of calls under an account
*/
router.get('/:sid/Calls', async(req, res) => {
const accountSid = req.params.sid;
const {logger, listCalls} = req.app.locals;
try {
const calls = await listCalls(accountSid);
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(snakeCase(calls));
} catch (err) {
sysError(logger, res, err);
}
});
/**
* retrieve single call
*/
router.get('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const callInfo = await retrieveCall(accountSid, callSid);
if (callInfo) {
logger.debug(callInfo, `retrieved call info for call sid ${callSid}`);
res.status(200).json(snakeCase(callInfo));
}
else {
logger.debug(`call not found for call sid ${callSid}`);
res.sendStatus(404);
}
} catch (err) {
sysError(logger, res, err);
}
});
/**
* delete call
*/
router.delete('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, deleteCall} = req.app.locals;
try {
const result = await deleteCall(accountSid, callSid);
if (result) {
logger.debug(`successfully deleted call ${callSid}`);
res.sendStatus(204);
}
else {
logger.debug(`call not found for call sid ${callSid}`);
res.sendStatus(404);
}
} catch (err) {
sysError(logger, res, err);
}
});
/**
* update a call
*/
router.post('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const call = await retrieveCall(accountSid, callSid);
if (call) {
const url = `${call.serviceUrl}/${API_VERSION}/updateCall/${callSid}`;
logger.debug({call, url, payload: req.body}, `updateCall: retrieved call info for call sid ${callSid}`);
request({
url: url,
method: 'POST',
json: true,
body: req.body
}, (err, response, body) => {
if (err) {
logger.error(err, `updateCall: Error sending update call POST to ${url}`);
return res.sendStatus(500);
}
res.sendStatus(response.statusCode);
});
}
else {
logger.debug(`updateCall: call not found for call sid ${callSid}`);
res.sendStatus(404);
}
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;
+134 -20
View File
@@ -1094,7 +1094,6 @@ paths:
to:
$ref: '#/components/schemas/Target'
description: destination for call
responses:
201:
description: call successfully created
@@ -1110,7 +1109,117 @@ paths:
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
get:
summary: list calls
operationId: listCalls
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
responses:
200:
description: list of calls for a specified account
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Call'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Calls/{CallSid}:
parameters:
- name: AccountSid
in: path
required: true
style: simple
explode: false
schema:
type: string
- name: CallSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
summary: delete a call
operationId: deleteCall
responses:
204:
description: call successfully deleted
404:
description: call not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: retrieve a call
responses:
200:
description: call found
content:
application/json:
schema:
$ref: '#/components/schemas/Call'
404:
description: call not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
post:
summary: update a call
requestBody:
content:
application/json:
schema:
type: object
properties:
call_hook:
$ref: '#/components/schemas/Webhook'
call_status_hook:
$ref: '#/components/schemas/Webhook'
call_status:
type: string
enum:
- completed
- no-answer
responses:
200:
description: call updated
content:
application/json:
schema:
$ref: '#/components/schemas/Call'
404:
description: call not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
components:
securitySchemes:
bearerAuth:
@@ -1331,16 +1440,24 @@ components:
application_sid:
type: string
format: uuid
api_version:
type: string
caller_name:
call_id:
type: string
call_sid:
type: string
format: uuid
date_created:
call_status:
type: string
date_updated:
enum:
- trying
- ringing
- alerting
- in-progress
- completed
- busy
- no-answer
- failed
- queued
caller_name:
type: string
direction:
type: string
@@ -1349,32 +1466,29 @@ components:
- outbound
duration:
type: integer
end_time:
type: string
forwarded_from:
type: string
from:
type: string
originating_sip_trunk_name:
type: string
parent_call_sid:
type: string
format: uuid
phone_number_sid:
type: string
format: uuid
start_time:
service_url:
type: string
sip_status:
type: integer
to:
type: string
uri:
type: string
required:
- api_version
- account-sid
- account_sid
- call_id
- call_sid
- date_created
- call_status
- direction
- from
- service_url
- sip_status
- to
- uri
Target:
properties:
type:
+22
View File
@@ -0,0 +1,22 @@
function snake(input) {
return input.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
function snakeCase(obj) {
if (Array.isArray(obj)) {
obj.forEach((r) => snakeCase(r));
}
else if (typeof obj === 'object' && obj !== null) {
Object.keys(obj).forEach((key) => {
obj[snake(key)] = obj[key];
delete obj[key];
});
}
else if (typeof obj === 'string') {
obj = snake(obj);
}
return obj;
}
module.exports = snakeCase;
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
"version": "1.1.0",
"version": "1.1.1",
"description": "",
"main": "app.js",
"scripts": {
@@ -19,6 +19,7 @@
"cors": "^2.8.5",
"express": "^4.17.1",
"jambonz-db-helpers": "^0.2.0",
"jambonz-realtimedb-helpers": "0.1.3",
"mysql2": "^2.0.2",
"passport": "^0.4.0",
"passport-http-bearer": "^1.0.1",