From 090bfbce926275315019fa27d86c58a3207068bc Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Sat, 5 Mar 2022 15:22:41 -0500 Subject: [PATCH] Feature/incoming refer (#39) * LCC under Kubernetes must use service name for FS (#35) * add api to send sip requests on a call (e.g NOTIFY, INFO) --- .eslintrc.json | 2 +- db/jambones-sql.sql | 2 +- lib/routes/api/accounts.js | 54 +++++++++++++++++++++-------------- lib/routes/api/sms-inbound.js | 45 ++++++++++++++++++----------- lib/swagger/swagger.yaml | 24 +++++++++++++--- package.json | 2 +- 6 files changed, 84 insertions(+), 45 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 03e3cfe..fecc21e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,7 @@ "jsx": false, "modules": false }, - "ecmaVersion": 2018 + "ecmaVersion": 2020 }, "plugins": ["promise"], "rules": { diff --git a/db/jambones-sql.sql b/db/jambones-sql.sql index 3614fa1..536885b 100644 --- a/db/jambones-sql.sql +++ b/db/jambones-sql.sql @@ -365,7 +365,7 @@ CREATE TABLE webhooks ( webhook_sid CHAR(36) NOT NULL UNIQUE , url VARCHAR(1024) NOT NULL, -method ENUM("GET","POST") NOT NULL DEFAULT 'POST', +method ENUM("GET","POST","WS") NOT NULL DEFAULT 'POST', username VARCHAR(255), password VARCHAR(255), PRIMARY KEY (webhook_sid) diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index 923cbd3..1345232 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -19,6 +19,23 @@ const translator = short(); let idx = 0; +const getFsUrl = async(logger, retrieveSet, setName) => { + if (process.env.K8S) return `http://${process.env.K8S_FEATURE_SERVER_SERVICE_NAME}:3000/v1/createCall`; + + try { + const fs = await retrieveSet(setName); + if (0 === fs.length) { + logger.info('No available feature servers to handle createCall API request'); + return ; + } + const ip = stripPort(fs[idx++ % fs.length]); + logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`); + return `http://${ip}:3000/v1/createCall`; + } catch (err) { + logger.error({err}, 'getFsUrl: error retreving feature servers from redis'); + } +}; + const stripPort = (hostport) => { const arr = /^(.*):(.*)$/.exec(hostport); if (arr) return arr[1]; @@ -99,7 +116,9 @@ function validateUpdateCall(opts) { 'listen_status', 'conf_hold_status', 'conf_mute_status', - 'mute_status'] + 'mute_status', + 'sip_request' + ] .reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0); switch (count) { @@ -133,6 +152,10 @@ function validateUpdateCall(opts) { if (opts.conf_mute_status && !['mute', 'unmute'].includes(opts.conf_mute_status)) { throw new DbErrorBadRequest('invalid conf_mute_status'); } + if (opts.sip_request && + (!opts.sip_request.method && !opts.sip_request.content_type || !opts.sip_request.content_type)) { + throw new DbErrorBadRequest('sip_request requires content_type and content properties'); + } } function validateTo(to) { @@ -567,18 +590,11 @@ router.post('/:sid/Calls', async(req, res) => { const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`; const {retrieveSet, logger} = req.app.locals; + const serviceUrl = await getFsUrl(logger, retrieveSet, setName); + if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480); try { - const fs = await retrieveSet(setName); - if (0 === fs.length) { - logger.info('No available feature servers to handle createCall API request'); - return res.json({msg: 'no available feature servers at this time'}).status(500); - } - const ip = stripPort(fs[idx++ % fs.length]); - logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`); - const serviceUrl = `http://${ip}:3000/v1/createCall`; await validateCreateCall(logger, sid, req); - logger.debug({payload: req.body}, `sending createCall API request to to ${ip}`); updateLastUsed(logger, sid, req).catch((err) => {}); request({ url: serviceUrl, @@ -587,11 +603,11 @@ router.post('/:sid/Calls', async(req, res) => { body: Object.assign(req.body, {account_sid: sid}) }, (err, response, body) => { if (err) { - logger.error(err, `Error sending createCall POST to ${ip}`); + logger.error(err, `Error sending createCall POST to ${serviceUrl}`); return res.sendStatus(500); } if (response.statusCode !== 201) { - logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${ip}`); + logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`); return res.sendStatus(500); } res.status(201).json(body); @@ -714,14 +730,8 @@ router.post('/:sid/Messages', async(req, res) => { const {retrieveSet, logger} = req.app.locals; try { - const fs = await retrieveSet(setName); - if (0 === fs.length) { - logger.info('No available feature servers to handle createMessage API request'); - return res.json({msg: 'no available feature servers at this time'}).status(500); - } - const ip = stripPort(fs[idx++ % fs.length]); - logger.info({fs}, `feature servers available for createMessage API request, selecting ${ip}`); - const serviceUrl = `http://${ip}:3000/v1/createMessage/${account_sid}`; + const serviceUrl = await getFsUrl(logger, retrieveSet, setName); + if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480); await validateCreateMessage(logger, account_sid, req); const payload = { @@ -729,7 +739,7 @@ router.post('/:sid/Messages', async(req, res) => { account_sid, ...req.body }; - logger.debug({payload}, `sending createMessage API request to to ${ip}`); + logger.debug({payload}, `sending createMessage API request to to ${serviceUrl}`); updateLastUsed(logger, account_sid, req).catch(() => {}); request({ url: serviceUrl, @@ -738,7 +748,7 @@ router.post('/:sid/Messages', async(req, res) => { body: payload }, (err, response, body) => { if (err) { - logger.error(err, `Error sending createMessage POST to ${ip}`); + logger.error(err, `Error sending createMessage POST to ${serviceUrl}`); return res.sendStatus(500); } if (response.statusCode !== 200) { diff --git a/lib/routes/api/sms-inbound.js b/lib/routes/api/sms-inbound.js index a4b34d2..b42351c 100644 --- a/lib/routes/api/sms-inbound.js +++ b/lib/routes/api/sms-inbound.js @@ -5,15 +5,37 @@ const { v4: uuidv4 } = require('uuid'); const sysError = require('../error'); let idx = 0; +const getFsUrl = async(logger, retrieveSet, setName, provider) => { + if (process.env.K8S) return `http://${process.env.K8S_FEATURE_SERVER_SERVICE_NAME}:3000/v1/messaging/${provider}`; -async function doSendResponse(res, respondFn, body) { + try { + const fs = await retrieveSet(setName); + if (0 === fs.length) { + logger.info('No available feature servers to handle createCall API request'); + return ; + } + const ip = stripPort(fs[idx++ % fs.length]); + logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`); + return `http://${ip}:3000/v1/messaging/${provider}`; + } catch (err) { + logger.error({err}, 'getFsUrl: error retreving feature servers from redis'); + } +}; + +const stripPort = (hostport) => { + const arr = /^(.*):(.*)$/.exec(hostport); + if (arr) return arr[1]; + return hostport; +}; + +const doSendResponse = async(res, respondFn, body) => { if (typeof respondFn === 'number') res.sendStatus(respondFn); else if (typeof respondFn !== 'function') res.sendStatus(200); else { const payload = await respondFn(body); res.status(200).json(payload); } -} +}; router.post('/:provider', async(req, res) => { const provider = req.params.provider; @@ -67,17 +89,8 @@ router.post('/:provider', async(req, res) => { } try { - const fs = await retrieveSet(setName); - if (0 === fs.length) { - logger.info('No available feature servers to handle createCall API request'); - return res - .json({ - msg: 'no available feature servers at this time' - }) - .status(480); - } - const ip = fs[idx++ % fs.length]; - const serviceUrl = `http://${ip}:3000/v1/messaging/${provider}`; + const serviceUrl = await getFsUrl(logger, retrieveSet, setName, provider); + if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480); const messageSid = uuidv4(); const payload = await Promise.resolve(filterFn({messageSid}, req.body)); @@ -113,7 +126,7 @@ router.post('/:provider', async(req, res) => { logger.debug({body: req.body, payload}, 'filtered incoming SMS'); - logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${ip}`); + logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${serviceUrl}`); request({ url: serviceUrl, @@ -123,7 +136,7 @@ router.post('/:provider', async(req, res) => { }, async(err, response, body) => { if (err) { - logger.error(err, `Error sending incomingSms POST to ${ip}`); + logger.error(err, `Error sending incomingSms POST to ${serviceUrl}`); return res.sendStatus(500); } if (200 === response.statusCode) { @@ -131,7 +144,7 @@ router.post('/:provider', async(req, res) => { logger.info({body}, 'sending response to provider for incomingSMS'); return doSendResponse(res, respondFn, body); } - logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${ip}`); + logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${serviceUrl}`); return res.sendStatus(500); }); } catch (err) { diff --git a/lib/swagger/swagger.yaml b/lib/swagger/swagger.yaml index 7e6a388..5e71d20 100644 --- a/lib/swagger/swagger.yaml +++ b/lib/swagger/swagger.yaml @@ -3102,9 +3102,12 @@ paths: enum: - completed - no-answer - conf_mute: - type: boolean - conf_status: + conf_mute_status: + type: string + enum: + - mute + - unmute + conf_hold_status: type: string enum: - hold @@ -3120,8 +3123,21 @@ paths: - mute - unmute whisper: - $ref: '#/components/schemas/Webhook' + $ref: '#/components/schemas/Webhook' + sip_request: + type: object + properties: + method: + type: string + content_type: + type: string + content: + type: string + headers: + type: object responses: + 200: + description: Accepted 202: description: Accepted 400: diff --git a/package.json b/package.json index 1a5583d..ef915c7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "app.js", "scripts": { "start": "node app.js", - "test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=info JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ ", + "test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ ", "integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js", "upgrade-db": "node ./db/upgrade-jambonz-db.js", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",