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)
This commit is contained in:
Dave Horton
2022-03-05 15:22:41 -05:00
committed by GitHub
parent 038e1d3917
commit 090bfbce92
6 changed files with 84 additions and 45 deletions

View File

@@ -8,7 +8,7 @@
"jsx": false, "jsx": false,
"modules": false "modules": false
}, },
"ecmaVersion": 2018 "ecmaVersion": 2020
}, },
"plugins": ["promise"], "plugins": ["promise"],
"rules": { "rules": {

View File

@@ -365,7 +365,7 @@ CREATE TABLE webhooks
( (
webhook_sid CHAR(36) NOT NULL UNIQUE , webhook_sid CHAR(36) NOT NULL UNIQUE ,
url VARCHAR(1024) NOT NULL, 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), username VARCHAR(255),
password VARCHAR(255), password VARCHAR(255),
PRIMARY KEY (webhook_sid) PRIMARY KEY (webhook_sid)

View File

@@ -19,6 +19,23 @@ const translator = short();
let idx = 0; 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 stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport); const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1]; if (arr) return arr[1];
@@ -99,7 +116,9 @@ function validateUpdateCall(opts) {
'listen_status', 'listen_status',
'conf_hold_status', 'conf_hold_status',
'conf_mute_status', 'conf_mute_status',
'mute_status'] 'mute_status',
'sip_request'
]
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0); .reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
switch (count) { switch (count) {
@@ -133,6 +152,10 @@ function validateUpdateCall(opts) {
if (opts.conf_mute_status && !['mute', 'unmute'].includes(opts.conf_mute_status)) { if (opts.conf_mute_status && !['mute', 'unmute'].includes(opts.conf_mute_status)) {
throw new DbErrorBadRequest('invalid 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) { 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 setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals; 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 { 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); await validateCreateCall(logger, sid, req);
logger.debug({payload: req.body}, `sending createCall API request to to ${ip}`);
updateLastUsed(logger, sid, req).catch((err) => {}); updateLastUsed(logger, sid, req).catch((err) => {});
request({ request({
url: serviceUrl, url: serviceUrl,
@@ -587,11 +603,11 @@ router.post('/:sid/Calls', async(req, res) => {
body: Object.assign(req.body, {account_sid: sid}) body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => { }, (err, response, body) => {
if (err) { if (err) {
logger.error(err, `Error sending createCall POST to ${ip}`); logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500); return res.sendStatus(500);
} }
if (response.statusCode !== 201) { 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); return res.sendStatus(500);
} }
res.status(201).json(body); res.status(201).json(body);
@@ -714,14 +730,8 @@ router.post('/:sid/Messages', async(req, res) => {
const {retrieveSet, logger} = req.app.locals; const {retrieveSet, logger} = req.app.locals;
try { try {
const fs = await retrieveSet(setName); const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (0 === fs.length) { if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
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}`;
await validateCreateMessage(logger, account_sid, req); await validateCreateMessage(logger, account_sid, req);
const payload = { const payload = {
@@ -729,7 +739,7 @@ router.post('/:sid/Messages', async(req, res) => {
account_sid, account_sid,
...req.body ...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(() => {}); updateLastUsed(logger, account_sid, req).catch(() => {});
request({ request({
url: serviceUrl, url: serviceUrl,
@@ -738,7 +748,7 @@ router.post('/:sid/Messages', async(req, res) => {
body: payload body: payload
}, (err, response, body) => { }, (err, response, body) => {
if (err) { if (err) {
logger.error(err, `Error sending createMessage POST to ${ip}`); logger.error(err, `Error sending createMessage POST to ${serviceUrl}`);
return res.sendStatus(500); return res.sendStatus(500);
} }
if (response.statusCode !== 200) { if (response.statusCode !== 200) {

View File

@@ -5,15 +5,37 @@ const { v4: uuidv4 } = require('uuid');
const sysError = require('../error'); const sysError = require('../error');
let idx = 0; 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); if (typeof respondFn === 'number') res.sendStatus(respondFn);
else if (typeof respondFn !== 'function') res.sendStatus(200); else if (typeof respondFn !== 'function') res.sendStatus(200);
else { else {
const payload = await respondFn(body); const payload = await respondFn(body);
res.status(200).json(payload); res.status(200).json(payload);
} }
} };
router.post('/:provider', async(req, res) => { router.post('/:provider', async(req, res) => {
const provider = req.params.provider; const provider = req.params.provider;
@@ -67,17 +89,8 @@ router.post('/:provider', async(req, res) => {
} }
try { try {
const fs = await retrieveSet(setName); const serviceUrl = await getFsUrl(logger, retrieveSet, setName, provider);
if (0 === fs.length) { if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
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 messageSid = uuidv4(); const messageSid = uuidv4();
const payload = await Promise.resolve(filterFn({messageSid}, req.body)); 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.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({ request({
url: serviceUrl, url: serviceUrl,
@@ -123,7 +136,7 @@ router.post('/:provider', async(req, res) => {
}, },
async(err, response, body) => { async(err, response, body) => {
if (err) { if (err) {
logger.error(err, `Error sending incomingSms POST to ${ip}`); logger.error(err, `Error sending incomingSms POST to ${serviceUrl}`);
return res.sendStatus(500); return res.sendStatus(500);
} }
if (200 === response.statusCode) { if (200 === response.statusCode) {
@@ -131,7 +144,7 @@ router.post('/:provider', async(req, res) => {
logger.info({body}, 'sending response to provider for incomingSMS'); logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body); 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); return res.sendStatus(500);
}); });
} catch (err) { } catch (err) {

View File

@@ -3102,9 +3102,12 @@ paths:
enum: enum:
- completed - completed
- no-answer - no-answer
conf_mute: conf_mute_status:
type: boolean type: string
conf_status: enum:
- mute
- unmute
conf_hold_status:
type: string type: string
enum: enum:
- hold - hold
@@ -3120,8 +3123,21 @@ paths:
- mute - mute
- unmute - unmute
whisper: 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: responses:
200:
description: Accepted
202: 202:
description: Accepted description: Accepted
400: 400:

View File

@@ -5,7 +5,7 @@
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "node app.js", "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", "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", "upgrade-db": "node ./db/upgrade-jambonz-db.js",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",