mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2025-12-19 05:47:46 +00:00
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:
@@ -8,7 +8,7 @@
|
|||||||
"jsx": false,
|
"jsx": false,
|
||||||
"modules": false
|
"modules": false
|
||||||
},
|
},
|
||||||
"ecmaVersion": 2018
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
"plugins": ["promise"],
|
"plugins": ["promise"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -3121,7 +3124,20 @@ paths:
|
|||||||
- 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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user