sms working for 3 providers

This commit is contained in:
Dave Horton
2020-10-07 14:40:56 -04:00
parent cff276402f
commit 6efbc49d4f
7 changed files with 186 additions and 55 deletions
+2
View File
@@ -33,6 +33,7 @@ const {
const {
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
lookupAppByPhoneNumber
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
@@ -55,6 +56,7 @@ Object.assign(app.locals, {
retrieveSet,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
lookupAppByPhoneNumber
});
+67
View File
@@ -135,6 +135,31 @@ async function validateCreateCall(logger, sid, req) {
}
}
async function validateCreateMessage(logger, sid, req) {
const obj = req.body;
const {lookupAccountByPhoneNumber} = req.app.locals;
if (req.user.account_sid !== sid) {
throw new DbErrorBadRequest(`unauthorized createMessage request for account ${sid}`);
}
if (!obj.from) throw new DbErrorBadRequest('missing from property');
else {
const regex = /^\+(\d+)$/;
const arr = regex.exec(obj.from);
const from = arr ? arr[1] : obj.from;
const account = await lookupAccountByPhoneNumber(from);
if (!account) throw new DbErrorBadRequest(`accountSid ${sid} does not own phone number ${from}`);
}
if (!obj.to) throw new DbErrorBadRequest('missing to property');
//obj.to = typeof obj.to === 'string' ? [obj.to] : obj.to;
if (!obj.text && (!obj.media || 0 === obj.media.length)) {
throw new DbErrorBadRequest('either text or media required in outbound message');
}
}
async function validateAdd(req) {
/* account-level token can not be used to add accounts */
if (req.user.hasAccountAuth) {
@@ -420,5 +445,47 @@ router.post('/:sid/Calls/:callSid', async(req, res) => {
}
});
/**
* create a new Message
*/
router.post('/:sid/Messages', async(req, res) => {
const sid = req.params.sid;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
try {
const fs = await retrieveSet(setName);
if (0 === fs.length) {
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 = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createMessage API request, selecting ${ip}`);
const serviceUrl = `http://${ip}:3000/v1/createMessage/${sid}`;
await validateCreateMessage(logger, sid, req);
const payload = req.body;
logger.debug({payload}, `sending createMessage API request to to ${ip}`);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: payload
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createMessage POST to ${ip}`);
return res.sendStatus(500);
}
if (response.statusCode !== 200) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`);
return res.sendStatus(500);
}
res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;
+22 -19
View File
@@ -1,6 +1,6 @@
const router = require('express').Router();
const request = require('request');
const smsPartnerFn = require('../../utils/sms-partner');
const getProvider = require('../../utils/sms-provider');
const uuidv4 = require('uuid/v4');
const sysError = require('./error');
let idx = 0;
@@ -15,44 +15,44 @@ async function doSendResponse(res, respondFn, body) {
}
}
router.post('/:partner', async(req, res) => {
const partnerName = req.params.partner;
router.post('/:provider', async(req, res) => {
const provider = req.params.provider;
const {
retrieveSet,
lookupAppByPhoneNumber,
logger
} = req.app.locals;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
const getPartner = smsPartnerFn(logger);
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
// search for partner module
const arr = getPartner(partnerName);
// search for provider module
const arr = getProvider(logger, provider);
if (!arr) {
logger.info(
`rejecting incomingSms request from unknown or badly configured partner ${partnerName}`
logger.info({body: req.body, params: req.params},
`rejecting incomingSms request from unknown provider ${provider}`
);
return res.sendStatus(404);
}
const partnerData = arr[1];
if (!partnerData || !partnerData.module) {
logger.info(
`rejecting incomingSms request from unknown or badly configured partner ${partnerName}`
const providerData = arr[1];
if (!providerData || !providerData.module) {
logger.info({body: req.body, params: req.params},
`rejecting incomingSms request from badly configured provider ${provider}`
);
return res.sendStatus(404);
}
// load partner module
// load provider module
let filterFn, respondFn;
try {
const {
fromProviderFormat,
formatProviderResponse
} = require(partnerData.module);
} = require(providerData.module);
// must at least provide a filter function
if (!fromProviderFormat) {
logger.info(
`missing fromProviderFormat function in module ${partnerData.module} for partner ${partnerName}`
`missing fromProviderFormat function in module ${providerData.module} for provider ${provider}`
);
return res.sendStatus(404);
}
@@ -61,7 +61,7 @@ router.post('/:partner', async(req, res) => {
} catch (err) {
logger.info(
err,
`failure loading module ${partnerData.module} for partner ${partnerName}`
`failure loading module ${providerData.module} for provider ${provider}`
);
return res.sendStatus(500);
}
@@ -77,7 +77,7 @@ router.post('/:partner', async(req, res) => {
.status(480);
}
const ip = fs[idx++ % fs.length];
const serviceUrl = `http://${ip}:3000/v1/messaging/${partnerName}`;
const serviceUrl = `http://${ip}:3000/v1/messaging/${provider}`;
const messageSid = uuidv4();
const payload = await Promise.resolve(filterFn({messageSid}, req.body));
@@ -86,7 +86,9 @@ router.post('/:partner', async(req, res) => {
* 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);
const to = Array.isArray(payload.to) ? payload.to : [payload.to];
const cc = Array.isArray(payload.cc) ? payload.cc : (payload.cc ? [payload.cc] : []);
const dids = to.concat(cc).filter((n) => n.length);
for (let did of dids) {
const regex = /^\+(\d+)$/;
const arr = regex.exec(did);
@@ -124,8 +126,9 @@ router.post('/:partner', async(req, res) => {
logger.error(err, `Error sending incomingSms POST to ${ip}`);
return res.sendStatus(500);
}
if (201 === response.statusCode) {
if (200 === response.statusCode) {
// success
logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body);
}
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${ip}`);
+17 -15
View File
@@ -1,39 +1,41 @@
const router = require('express').Router();
const smsPartnerFn = require('../../utils/sms-partner');
const getProvider = require('../../utils/sms-provider');
const sysError = require('./error');
router.post('/', async(req, res) => {
const { logger } = req.app.locals;
const getPartner = smsPartnerFn(logger);
try {
// if partner specified use it, otherwise use first in list
const arr = getPartner(req.body.partner);
// if provider specified use it, otherwise use first in list
const arr = getProvider(logger, req.body.provider);
if (!Array.isArray(arr)) {
throw new Error('outboundSMS - unable to locate sms provider to use to send message');
}
const partnerData = arr[1];
if (!partnerData || !partnerData.module) {
throw new Error(`rejecting outgoingSms request for unknown or badly configured partner ${req.body.partner}`);
const providerData = arr[1];
if (!providerData || !providerData.module) {
throw new Error(`rejecting outgoingSms request for unknown or badly configured provider ${req.body.provider}`);
}
const partnerName = arr[0];
const opts = partnerData.options;
const provider = arr[0];
const opts = providerData.options;
if (!opts || !opts.url) {
throw new Error(`rejecting outgoingSms request -- no HTTP url for ${req.body.partner}`);
throw new Error(`rejecting outgoingSms request -- no HTTP url for ${req.body.provider}`);
}
// load partner module
const { sendSms } = require(partnerData.module);
// load provider module
const { sendSms } = require(providerData.module);
if (!sendSms) {
throw new Error(`missing sendSms function in module ${partnerData.module} for partner ${partnerName}`);
throw new Error(`missing sendSms function in module ${providerData.module} for provider ${provider}`);
}
// send the SMS
const response = await sendSms(opts, req.body);
const payload = req.body;
delete payload.provider;
logger.debug({opts, payload}, `outboundSMS - sending to ${opts.url}`);
const response = await sendSms(opts, payload);
logger.info({response, payload: req.body}, `outboundSMS - sent to ${opts.url}`);
res.status(200).json({'status': 'ok'});
res.status(200).json(response);
} catch (err) {
sysError(logger, res, err);
}
+57 -5
View File
@@ -1436,9 +1436,9 @@ paths:
application/json:
schema:
required:
- callSid
- sid
properties:
callSid:
sid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
@@ -1468,8 +1468,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Calls/{CallSid}:
parameters:
- name: AccountSid
@@ -1570,7 +1568,37 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Messages:
post:
summary: create an outgoing SMS message
operationId: createMessage
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
responses:
201:
description: call successfully created
content:
application/json:
schema:
required:
- sid
properties:
sid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
400:
description: bad request
components:
securitySchemes:
bearerAuth:
@@ -1918,6 +1946,30 @@ components:
required:
- type
example: {"type": "phone", "number": "+16172375080"}
Message:
properties:
provider:
type: string
from:
type: string
to:
type: array
items:
type: string
cc:
type: array
items:
type: string
text:
type: string
media:
type: array
items:
type: string
required:
- from
- to
example: {"from": "13394445678", "to": "16173333456", "text": "please call when you can"}
security:
- bearerAuth: []
@@ -1,14 +1,16 @@
const partners = new Map();
const providers = new Map();
let init = false;
function initPartners(logger) {
function initProviders(logger) {
if (init) return;
if (process.env.JAMBONES_MESSAGING) {
try {
const obj = JSON.parse(process.env.JAMBONES_MESSAGING);
for (const [key, value] of Object.entries(obj)) {
logger.debug({config: value}, `Adding SMS provider ${key}`);
partners.set(key, value);
providers.set(key, value);
}
logger.info(`Configured ${partners.size} SMS partners`);
logger.info(`Configured ${providers.size} SMS providers`);
} catch (err) {
logger.error(err, `expected JSON for JAMBONES_MESSAGING : ${process.env.JAMBONES_MESSAGING}`);
}
@@ -16,23 +18,23 @@ function initPartners(logger) {
else {
logger.info('no JAMBONES_MESSAGING env var, messaging is disabled');
}
init = true;
}
function getPartner(logger, partner) {
function getProvider(logger, partner) {
initProviders(logger);
if (typeof partner === 'string') {
const config = partners.get(partner);
const config = providers.get(partner);
const arr = [partner, config];
logger.debug({arr}, 'getPartner by name');
logger.debug({arr}, 'getProvider by name');
return arr;
}
else if (partners.size) {
const arr = partners.entries().next().value;
logger.debug({arr}, 'getPartner by first available');
else if (providers.size) {
const arr = providers.entries().next().value;
logger.debug({arr}, 'getProvider by first available');
return arr;
}
}
module.exports = (logger) => {
initPartners(logger);
return getPartner.bind(null, logger);
};
module.exports = getProvider;
+5 -2
View File
@@ -15,8 +15,10 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@jambonz/db-helpers": "^0.5.0",
"@jambonz/messaging-peerless": "0.0.5",
"@jambonz/db-helpers": "^0.5.1",
"@jambonz/messaging-382com": "0.0.2",
"@jambonz/messaging-peerless": "0.0.6",
"@jambonz/messaging-simwood": "0.0.4",
"@jambonz/realtimedb-helpers": "0.2.17",
"cors": "^2.8.5",
"express": "^4.17.1",
@@ -26,6 +28,7 @@
"pino": "^5.17.0",
"request": "^2.88.2",
"request-debug": "^0.2.0",
"swagger-ui-dist": "^3.35.0",
"swagger-ui-express": "^4.1.4",
"uuid": "^3.4.0",
"yamljs": "^0.3.0"