mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-07-19 02:21:54 +00:00
inbound SMS
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
const assert = require('assert');
|
||||
const opts = Object.assign({
|
||||
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
|
||||
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
|
||||
timestamp: () => {
|
||||
return `, "time": "${new Date().toISOString()}"`;
|
||||
}
|
||||
}, {
|
||||
level: process.env.JAMBONES_LOGLEVEL || 'info'
|
||||
});
|
||||
const logger = require('pino')(opts);
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
@@ -28,7 +32,8 @@ const {
|
||||
}, logger);
|
||||
const {
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid
|
||||
lookupAccountBySid,
|
||||
lookupAppByPhoneNumber
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
@@ -49,7 +54,8 @@ Object.assign(app.locals, {
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid
|
||||
lookupAccountBySid,
|
||||
lookupAppByPhoneNumber
|
||||
});
|
||||
|
||||
const unless = (paths, middleware) => {
|
||||
@@ -60,13 +66,19 @@ const unless = (paths, middleware) => {
|
||||
};
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use('/v1', unless(['/login', '/Users'], passport.authenticate('bearer', { session: false })));
|
||||
app.use('/v1', unless(['/login', '/Users', '/messaging'], passport.authenticate('bearer', {
|
||||
session: false
|
||||
})));
|
||||
app.use('/', routes);
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error(err, 'burped error');
|
||||
res.status(err.status || 500).json({msg: err.message});
|
||||
res.status(err.status || 500).json({
|
||||
msg: err.message
|
||||
});
|
||||
});
|
||||
logger.info(`listening for HTTP traffic on port ${PORT}`);
|
||||
app.listen(PORT);
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
const Model = require('./model');
|
||||
const {getMysqlConnection} = require('../db');
|
||||
const listSqlSp = `
|
||||
SELECT * from applications
|
||||
WHERE account_sid in (
|
||||
SELECT account_sid from accounts
|
||||
WHERE service_provider_sid = ?
|
||||
)`;
|
||||
const listSqlAccount = 'SELECT * from applications WHERE account_sid = ?';
|
||||
const retrieveSqlSp = `
|
||||
SELECT * from applications
|
||||
WHERE account_sid in (
|
||||
SELECT account_sid from accounts
|
||||
WHERE service_provider_sid = ?
|
||||
)
|
||||
AND application_sid = ?`;
|
||||
const retrieveSqlAccount = `
|
||||
SELECT * from applications
|
||||
WHERE account_sid = ?
|
||||
AND application_sid = ?`;
|
||||
|
||||
const retrieveSql = `SELECT * from applications app
|
||||
LEFT JOIN webhooks AS ch
|
||||
|
||||
+89
-29
@@ -1,5 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const request = require('request');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const sysError = require('./error');
|
||||
const partners = {};
|
||||
let initialized = false;
|
||||
@@ -13,43 +14,68 @@ function initPartners(logger) {
|
||||
try {
|
||||
const obj = JSON.parse(process.env.JAMBONES_MESSAGING);
|
||||
Object.assign(partners, obj);
|
||||
logger.info({partners}, 'Messaging partners configuration');
|
||||
logger.info({
|
||||
partners
|
||||
}, 'Messaging partners configuration');
|
||||
} catch (err) {
|
||||
logger.error(err, `expected JSON for JAMBONES_MESSAGING : ${process.env.JAMBONES_MESSAGING}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.info('no JAMBONES_MESSAGING env var, messaging is disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async function doSendResponse(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('/:partner', async(req, res) => {
|
||||
const partner = req.params.partner;
|
||||
const {retrieveSet, logger} = req.app.locals;
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||
const {
|
||||
retrieveSet,
|
||||
lookupAppByPhoneNumber,
|
||||
logger
|
||||
} = req.app.locals;
|
||||
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
|
||||
|
||||
// one time load of partner-specific filter and respond functions
|
||||
// search for partner module
|
||||
initPartners(logger);
|
||||
|
||||
const partnerModule = partners[partner];
|
||||
if (!partnerModule) {
|
||||
logger.info(`rejecting incomingSms request from unknown partner ${partner}`);
|
||||
const partnerData = partners[partner];
|
||||
if (!partnerData || !partnerData.module || !partnerData.options) {
|
||||
logger.info(
|
||||
`rejecting incomingSms request from unknown or badly configured partner ${partner}`
|
||||
);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
// load partner module
|
||||
let filterFn, respondFn;
|
||||
try {
|
||||
const {filter, respond} = require(partnerModule);
|
||||
const {
|
||||
fromProviderFormat,
|
||||
formatProviderResponse
|
||||
} = require(partnerData.module);
|
||||
// must at least provide a filter function
|
||||
if (!filter) {
|
||||
logger.info(`missing filter function in module ${partnerModule} for partner ${partner}`);
|
||||
if (!fromProviderFormat) {
|
||||
logger.info(
|
||||
`missing fromProviderFormat function in module ${partnerData.module} for partner ${partner}`
|
||||
);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
filterFn = filter;
|
||||
if (!respond) respondFn = res.sendStatus.bind(null, 200);
|
||||
else if (typeof respond === 'number' && respond >= 200 && respond < 299) {
|
||||
respondFn = res.sendStatus.bind(null, respond);
|
||||
}
|
||||
else respondFn;
|
||||
filterFn = fromProviderFormat;
|
||||
respondFn = formatProviderResponse;
|
||||
} catch (err) {
|
||||
logger.info(err, `failure loading module ${partnerModule} for partner ${partner}`);
|
||||
logger.info(
|
||||
err,
|
||||
`failure loading module ${partnerData.module} for partner ${partner}`
|
||||
);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
@@ -57,19 +83,59 @@ router.post('/:partner', async(req, res) => {
|
||||
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);
|
||||
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/${partner}`;
|
||||
const messageSid = uuidv4();
|
||||
const payload = await Promise.resolve(filterFn({messageSid}, req.body));
|
||||
|
||||
/**
|
||||
* lookup the application associated with the number in the To field
|
||||
* since there could be multiple Tos, we have to search through (and cc also)
|
||||
*/
|
||||
let app;
|
||||
const dids = [payload.to].concat([payload.cc]).filter((n) => n.length);
|
||||
for (let did of dids) {
|
||||
const regex = /^\+(\d+)$/;
|
||||
const arr = regex.exec(did);
|
||||
did = arr ? arr[1] : did;
|
||||
const obj = await lookupAppByPhoneNumber(did);
|
||||
logger.info({obj}, `lookup app for phone number ${did}`);
|
||||
if (obj) {
|
||||
logger.info({did, obj}, 'Found app for DID');
|
||||
app = obj;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!app) {
|
||||
logger.info({payload}, 'No application found for incoming SMS');
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
if (!app.messaging_hook) {
|
||||
logger.info({payload}, `app "${app.name}" found for incoming SMS does not have an associated messaging hook`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
payload.applicationSid = app.application_sid;
|
||||
payload.accountSid = app.account_sid;
|
||||
payload.messaging_hook = app.messaging_hook;
|
||||
|
||||
logger.debug({body: req.body, payload}, 'filtered incoming SMS');
|
||||
|
||||
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${ip}`);
|
||||
|
||||
const payload = Promise.resolve(filterFn(req.body, req));
|
||||
logger.debug({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${ip}`);
|
||||
request({
|
||||
url: serviceUrl,
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: payload
|
||||
}, async(err, response, body) => {
|
||||
body: payload,
|
||||
},
|
||||
async(err, response, body) => {
|
||||
if (err) {
|
||||
logger.error(err, `Error sending incomingSms POST to ${ip}`);
|
||||
return res.sendStatus(500);
|
||||
@@ -78,7 +144,7 @@ router.post('/:partner', async(req, res) => {
|
||||
// success
|
||||
return doSendResponse(res, respondFn, body);
|
||||
}
|
||||
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${ip}`);
|
||||
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${ip}`);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -86,10 +152,4 @@ router.post('/:partner', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function doSendResponse(res, respondFn, body) {
|
||||
const payload = await respondFn(body);
|
||||
if (typeof payload === 'number') return res.sendStatus(payload);
|
||||
else if (typeof payload === 'string') return res.status(200).json(payload);
|
||||
else return res.sendStatus(200);
|
||||
}
|
||||
module.exports = router;
|
||||
|
||||
@@ -31,13 +31,13 @@ async function validateAdd(req) {
|
||||
async function checkInUse(req, sid) {
|
||||
const phoneNumber = await PhoneNumber.retrieve(sid);
|
||||
if (phoneNumber.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
|
||||
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
|
||||
}
|
||||
}
|
||||
|
||||
/* can not change number or voip carrier */
|
||||
async function validateUpdate(req, sid) {
|
||||
const result = await PhoneNumber.retrieve(sid);
|
||||
//const result = await PhoneNumber.retrieve(sid);
|
||||
if (req.body.voip_carrier_sid) throw new DbErrorBadRequest('voip_carrier_sid may not be modified');
|
||||
if (req.body.number) throw new DbErrorBadRequest('number may not be modified');
|
||||
|
||||
|
||||
@@ -97,6 +97,4 @@ router.put('/:sid', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.5.0",
|
||||
"@jambonz/messaging-peerless": "0.0.4",
|
||||
"@jambonz/realtimedb-helpers": "0.2.16",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
|
||||
Reference in New Issue
Block a user