From 8a913337259ca286e4d4fe895976b0c394f1ff03 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Wed, 30 Sep 2020 15:28:12 -0400 Subject: [PATCH] inbound SMS --- app.js | 26 ++++-- lib/models/application.js | 18 ----- lib/routes/api/messaging.js | 118 +++++++++++++++++++++------- lib/routes/api/phone-numbers.js | 4 +- lib/routes/api/service-providers.js | 2 - package.json | 1 + 6 files changed, 111 insertions(+), 58 deletions(-) diff --git a/app.js b/app.js index 7a78bf6..ac7f856 100644 --- a/app.js +++ b/app.js @@ -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); diff --git a/lib/models/application.js b/lib/models/application.js index 8b2f73e..1320d6b 100644 --- a/lib/models/application.js +++ b/lib/models/application.js @@ -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 diff --git a/lib/routes/api/messaging.js b/lib/routes/api/messaging.js index 727a964..54da629 100644 --- a/lib/routes/api/messaging.js +++ b/lib/routes/api/messaging.js @@ -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; diff --git a/lib/routes/api/phone-numbers.js b/lib/routes/api/phone-numbers.js index 7d7a6ba..6ef6bee 100644 --- a/lib/routes/api/phone-numbers.js +++ b/lib/routes/api/phone-numbers.js @@ -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'); diff --git a/lib/routes/api/service-providers.js b/lib/routes/api/service-providers.js index 50de45e..048b148 100644 --- a/lib/routes/api/service-providers.js +++ b/lib/routes/api/service-providers.js @@ -97,6 +97,4 @@ router.put('/:sid', async(req, res) => { } }); - - module.exports = router; diff --git a/package.json b/package.json index dcc920d..f82d29d 100644 --- a/package.json +++ b/package.json @@ -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",