inbound SMS

This commit is contained in:
Dave Horton
2020-09-30 15:28:12 -04:00
parent ee27cf41cb
commit 8a91333725
6 changed files with 111 additions and 58 deletions
+19 -7
View File
@@ -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);
-18
View File
@@ -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
View File
@@ -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;
+2 -2
View File
@@ -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');
-2
View File
@@ -97,6 +97,4 @@ router.put('/:sid', async(req, res) => {
}
});
module.exports = router;
+1
View File
@@ -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",