From ec02052d27579a76c2edf87504d98359760bf838 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Fri, 7 Feb 2020 10:18:53 -0500 Subject: [PATCH] initial support for live call control --- app.js | 9 ++- config/default.json.example | 7 +- config/test.json | 4 + lib/routes/api/accounts.js | 106 ++++++++++++++++++++++++- lib/swagger/swagger.yaml | 154 +++++++++++++++++++++++++++++++----- lib/utils/snake-case.js | 22 ++++++ package.json | 3 +- 7 files changed, 280 insertions(+), 25 deletions(-) create mode 100644 lib/utils/snake-case.js diff --git a/app.js b/app.js index eaebb5b..353104a 100644 --- a/app.js +++ b/app.js @@ -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 })); diff --git a/config/default.json.example b/config/default.json.example index 7f6216e..11c4ab0 100644 --- a/config/default.json.example +++ b/config/default.json.example @@ -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" } } \ No newline at end of file diff --git a/config/test.json b/config/test.json index 89b8f6d..0b73c31 100644 --- a/config/test.json +++ b/config/test.json @@ -7,5 +7,9 @@ "user": "jambones_test", "database": "jambones_test", "password": "jambones_test" + }, + "redis": { + "host": "127.0.0.1", + "port": 6379 } } \ No newline at end of file diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index 7d7957c..fa8d179 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -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; diff --git a/lib/swagger/swagger.yaml b/lib/swagger/swagger.yaml index 4d95090..42e4832 100644 --- a/lib/swagger/swagger.yaml +++ b/lib/swagger/swagger.yaml @@ -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: diff --git a/lib/utils/snake-case.js b/lib/utils/snake-case.js new file mode 100644 index 0000000..3bce88e --- /dev/null +++ b/lib/utils/snake-case.js @@ -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; diff --git a/package.json b/package.json index 075615f..cb43843 100644 --- a/package.json +++ b/package.json @@ -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",