From 2a974704fc9f39011bf1c43dd8cb8ab9d5c5c833 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Sat, 1 Feb 2020 16:15:29 -0500 Subject: [PATCH] added initial support for creating calls through REST API --- app.js | 1 + config/default.json.example | 3 + lib/routes/api/accounts.js | 81 ++++++++++++++++++ lib/swagger/swagger.yaml | 161 +++++++++++++++++++++--------------- package.json | 1 + 5 files changed, 182 insertions(+), 65 deletions(-) diff --git a/app.js b/app.js index b61ce7b..eaebb5b 100644 --- a/app.js +++ b/app.js @@ -24,6 +24,7 @@ app.use((err, req, res, next) => { logger.error(err, 'burped error'); res.status(err.status || 500).json({msg: err.message}); }); +logger.info(`listening for HTTP traffic on port ${PORT}`); app.listen(PORT); module.exports = app; diff --git a/config/default.json.example b/config/default.json.example index 21cc8b8..7f6216e 100644 --- a/config/default.json.example +++ b/config/default.json.example @@ -8,5 +8,8 @@ "password": "secret", "database": "jambones", "connectionLimit": 10 + }, + "services": { + "createCall": "http://feature.server/createCall:3000" } } \ No newline at end of file diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index 2517c90..1768862 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -1,4 +1,6 @@ const router = require('express').Router(); +const request = require('request'); +const config = require('config'); const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); const Account = require('../../models/account'); const Webhook = require('../../models/webhook'); @@ -11,6 +13,54 @@ const preconditions = { 'delete': validateDelete }; +function validateTo(to) { + if (to && typeof to === 'object') { + switch (to.type) { + case 'phone': + if (typeof to.number === 'string') return; + break; + case 'user': + if (typeof to.name === 'string') return; + break; + case 'sip': + if (typeof to.sipUri === 'string') return; + break; + } + } + throw new DbErrorBadRequest(`missing or invalid to property: ${to}`); +} +async function validateCreateCall(logger, sid, req) { + const {lookupApplicationBySid} = require('jambonz-db-helpers')(config.get('mysql'), logger); + const obj = req.body; + + if (req.user.account_sid !== sid) throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`); + + if (!obj.from) throw new DbErrorBadRequest('missing from parameter'); + validateTo(obj.to); + + if (obj.application_sid) { + try { + logger.debug(`Accounts:validateCreateCall retrieving application ${obj.application_sid}`); + const application = await lookupApplicationBySid(obj.application_sid); + logger.debug(`Accounts:validateCreateCall retrieved application ${JSON.stringify(application)}`); + Object.assign(obj, { + call_hook: application.call_hook, + call_status_hook: application.call_status_hook, + speech_synthesis_vendor: application.speech_synthesis_vendor, + speech_synthesis_voice: application.speech_synthesis_voice, + speech_recognizer_vendor: application.speech_recognizer_vendor, + speech_recognizer_language: application.speech_recognizer_language + }); + logger.debug({obj, application}, 'Accounts:validateCreateCall augmented with application settings'); + } catch (err) { + logger.error(err, `Accounts:validateCreateCall error retrieving application for sid ${obj.application_sid}`); + throw new DbErrorBadRequest(`application_sid not found ${obj.application_sid}`); + } + } + + if (!obj.call_hook || (obj.call_hook && !obj.call_hook.url)) throw new DbErrorBadRequest('either url or application_sid required'); +} + async function validateAdd(req) { /* account-level token can not be used to add accounts */ if (req.user.hasAccountAuth) { @@ -152,4 +202,35 @@ router.put('/:sid', async(req, res) => { } }); +router.post('/:sid/Calls', async(req, res) => { + const sid = req.params.sid; + const logger = req.app.locals.logger; + + try { + const serviceUrl = config.get('services.createCall'); + await validateCreateCall(logger, sid, req); + + logger.debug({payload: req.body}, `sending POST to ${serviceUrl}`); + request({ + url: serviceUrl, + method: 'POST', + json: true, + body: req.body + }, (err, response, body) => { + if (err) { + logger.error(err, `Error sending createCall POST to ${serviceUrl}`); + return res.send(500); + } + if (response.statusCode !== 201) { + logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`); + return res.sendStatus(500); + } + res.status(201).json(body); + }); + } catch (err) { + sysError(logger, res, err); + } + +}); + module.exports = router; diff --git a/lib/swagger/swagger.yaml b/lib/swagger/swagger.yaml index 29d2692..8251ae8 100644 --- a/lib/swagger/swagger.yaml +++ b/lib/swagger/swagger.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: - title: Jambones REST API - description: Jambones REST API + title: jambonz REST API + description: jambonz REST API contact: email: daveh@drachtio.org license: @@ -1051,7 +1051,7 @@ paths: $ref: '#/components/schemas/GeneralError' - /Accounts/{AccountSid}/Applications/{ApplicationSid}/Calls: + /Accounts/{AccountSid}/Calls: post: summary: create a call operationId: createCall @@ -1061,11 +1061,6 @@ paths: required: true schema: type: string - - name: ApplicationSid - in: path - required: true - schema: - type: string requestBody: content: application/json: @@ -1075,39 +1070,28 @@ paths: - to type: object properties: - url: + application_sid: type: string - description: | - The url of the web application to control this call. - If not provided, the url specified in the application will be used - example: https://mycompany.com/deliver-message.json + format: uuid + description: The application to use to control this call. Either applicationSid or url is required. + call_hook: + $ref: '#/components/Webhook' + call_status_hook: + $ref: '#/components/Webhook' from: type: string description: The calling party number - example: 16172375089 + example: +16172375089 + timeout: + type: integer + description: the number of seconds to wait for call to be answered. Defaults to 60. + tag: + type: object + description: initial set of customer-supplied metadata to associate with the call (see jambonz 'tag' verb) to: - type: string - description: The telephone number or sip endpoint to call - example: 16172228000 - recordingUrl: - type: string - format: url - description: A websocket url to stream the call audio to - example: wss://myserver.com - recordingMix: - type: string - description: whether to record either or both parties - enum: - - caller - - callee - - stereo - - mixed - example: stereo - statusCallback: - type: string - format: url - description: The url to send call status change events to - example: https://company.com/status + $ref: '#/components/Target' + description: destination for call + responses: 201: description: call successfully created @@ -1317,35 +1301,6 @@ components: - username - domain - sip_contact - Call: - type: object - properties: - call_sid: - type: string - format: uuid - application: - $ref: '#/components/schemas/Application' - parent_call: - $ref: '#/components/schemas/Call' - direction: - type: string - enum: - - inbound - - outbound - phone_number: - $ref: '#/components/schemas/PhoneNumber' - inbound_user: - $ref: '#/components/schemas/RegisteredUser' - outbound_user: - $ref: '#/components/schemas/RegisteredUser' - calling_number: - type: string - called_number: - type: string - required: - - call_sid - - application - - direction Webhook: type: object properties: @@ -1363,6 +1318,82 @@ components: type: string required: - url + Call: + type: object + properties: + account_sid: + type: string + format: uuid + application_sid: + type: string + format: uuid + api_version: + type: string + caller_name: + type: string + call_sid: + type: string + format: uuid + date_created: + type: string + date_updated: + type: string + direction: + type: string + enum: + - inbound + - outbound + duration: + type: integer + end_time: + type: string + forwarded_from: + type: string + from: + type: string + parent_call_sid: + type: string + format: uuid + phone_number_sid: + type: string + format: uuid + start_time: + type: string + to: + type: string + uri: + type: string + required: + - api_version + - account-sid + - call_sid + - date_created + - from + - to + - uri + Target: + properties: + type: + type: string + enum: + - phone + - sip + - user + number: + type: string + sipUri: + type: string + name: + type: string + auth: + type: object + properties: + username: + type: string + password: + type: string + required: + - type security: - bearerAuth: [] \ No newline at end of file diff --git a/package.json b/package.json index 18089bc..2a4460a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "config": "^3.2.4", "cors": "^2.8.5", "express": "^4.17.1", + "jambonz-db-helpers": "^0.2.0", "mysql2": "^2.0.2", "passport": "^0.4.0", "passport-http-bearer": "^1.0.1",