diff --git a/db/jambones-sql.sql b/db/jambones-sql.sql
index 950467b..c28c32a 100644
--- a/db/jambones-sql.sql
+++ b/db/jambones-sql.sql
@@ -148,8 +148,9 @@ CREATE TABLE applications
application_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
account_sid CHAR(36) NOT NULL COMMENT 'account that this application belongs to',
-call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls to phone numbers owned by this account',
+call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
+messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
@@ -242,6 +243,8 @@ ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REF
ALTER TABLE applications ADD FOREIGN KEY call_status_hook_sid_idxfk (call_status_hook_sid) REFERENCES webhooks (webhook_sid);
+ALTER TABLE applications ADD FOREIGN KEY messaging_hook_sid_idxfk (messaging_hook_sid) REFERENCES webhooks (webhook_sid);
+
CREATE INDEX service_provider_sid_idx ON service_providers (service_provider_sid);
CREATE INDEX name_idx ON service_providers (name);
CREATE INDEX root_domain_idx ON service_providers (root_domain);
diff --git a/db/jambones.sqs b/db/jambones.sqs
index b72bea8..a5c1046 100644
--- a/db/jambones.sqs
+++ b/db/jambones.sqs
@@ -194,11 +194,13 @@
+
+
@@ -764,7 +766,7 @@
345.00
- 240.00
+ 260.00
0
@@ -812,7 +814,7 @@
2
-
+
@@ -829,6 +831,20 @@
+
+
+
+ webhook_sid
+ webhooks
+
+
+ 4
+ 1
+
+
+
+
+
@@ -1083,17 +1099,17 @@
-
+
-
-
-
+
+
+
-
+
diff --git a/lib/models/application.js b/lib/models/application.js
index 58acf75..8b2f73e 100644
--- a/lib/models/application.js
+++ b/lib/models/application.js
@@ -23,7 +23,9 @@ const retrieveSql = `SELECT * from applications app
LEFT JOIN webhooks AS ch
ON app.call_hook_sid = ch.webhook_sid
LEFT JOIN webhooks AS sh
-ON app.call_status_hook_sid = sh.webhook_sid`;
+ON app.call_status_hook_sid = sh.webhook_sid
+LEFT JOIN webhooks AS mh
+ON app.messaging_hook_sid = mh.webhook_sid`;
function transmogrifyResults(results) {
return results.map((row) => {
@@ -36,8 +38,13 @@ function transmogrifyResults(results) {
Object.assign(obj, {call_status_hook: row.sh});
}
else obj.call_status_hook = null;
+ if (row.mh && Object.keys(row.mh).length && row.mh.url !== null) {
+ Object.assign(obj, {messaging_hook: row.mh});
+ }
+ else obj.messaging_hook = null;
delete obj.call_hook_sid;
delete obj.call_status_hook_sid;
+ delete obj.messaging_hook_sid;
return obj;
});
}
@@ -123,12 +130,14 @@ Application.fields = [
{
name: 'call_hook_sid',
type: 'string',
- required: true
},
{
name: 'call_status_hook_sid',
type: 'string',
- required: true
+ },
+ {
+ name: 'messaging_hook_sid',
+ type: 'string',
}
];
diff --git a/lib/routes/api/applications.js b/lib/routes/api/applications.js
index f833ec0..122f80c 100644
--- a/lib/routes/api/applications.js
+++ b/lib/routes/api/applications.js
@@ -62,7 +62,7 @@ router.post('/', async(req, res) => {
// create webhooks if provided
const obj = Object.assign({}, req.body);
- for (const prop of ['call_hook', 'call_status_hook']) {
+ for (const prop of ['call_hook', 'call_status_hook', 'messaging_hook']) {
if (obj[prop]) {
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
delete obj[prop];
@@ -113,7 +113,7 @@ router.put('/:sid', async(req, res) => {
// create webhooks if provided
const obj = Object.assign({}, req.body);
- for (const prop of ['call_hook', 'call_status_hook']) {
+ for (const prop of ['call_hook', 'call_status_hook', 'messaging_hook']) {
if (prop in obj && Object.keys(obj[prop]).length) {
if ('webhook_sid' in obj[prop]) {
const sid = obj[prop]['webhook_sid'];
diff --git a/lib/routes/api/index.js b/lib/routes/api/index.js
index eadf3aa..715ae93 100644
--- a/lib/routes/api/index.js
+++ b/lib/routes/api/index.js
@@ -20,4 +20,6 @@ api.use('/Sbcs', isAdminScope, require('./sbcs'));
api.use('/Users', require('./users'));
api.use('/login', require('./login'));
+api.use('/messaging', require('./messaging'));
+
module.exports = api;
diff --git a/lib/routes/api/messaging.js b/lib/routes/api/messaging.js
new file mode 100644
index 0000000..727a964
--- /dev/null
+++ b/lib/routes/api/messaging.js
@@ -0,0 +1,95 @@
+const router = require('express').Router();
+const request = require('request');
+const sysError = require('./error');
+const partners = {};
+let initialized = false;
+let idx = 0;
+
+function initPartners(logger) {
+ if (initialized) return;
+ initialized = true;
+
+ if (process.env.JAMBONES_MESSAGING) {
+ try {
+ const obj = JSON.parse(process.env.JAMBONES_MESSAGING);
+ Object.assign(partners, obj);
+ logger.info({partners}, 'Messaging partners configuration');
+ } catch (err) {
+ logger.error(err, `expected JSON for JAMBONES_MESSAGING : ${process.env.JAMBONES_MESSAGING}`);
+ }
+ }
+}
+
+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`;
+
+ // one time load of partner-specific filter and respond functions
+ initPartners(logger);
+
+ const partnerModule = partners[partner];
+ if (!partnerModule) {
+ logger.info(`rejecting incomingSms request from unknown partner ${partner}`);
+ return res.sendStatus(404);
+ }
+
+ let filterFn, respondFn;
+ try {
+ const {filter, respond} = require(partnerModule);
+ // must at least provide a filter function
+ if (!filter) {
+ logger.info(`missing filter function in module ${partnerModule} 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;
+ } catch (err) {
+ logger.info(err, `failure loading module ${partnerModule} for partner ${partner}`);
+ return res.sendStatus(500);
+ }
+
+ 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(480);
+ }
+ const ip = fs[idx++ % fs.length];
+ const serviceUrl = `http://${ip}:3000/v1/messaging/${partner}`;
+
+ 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) => {
+ if (err) {
+ logger.error(err, `Error sending incomingSms POST to ${ip}`);
+ return res.sendStatus(500);
+ }
+ if (201 === response.statusCode) {
+ // success
+ return doSendResponse(res, respondFn, body);
+ }
+ logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${ip}`);
+ return res.sendStatus(500);
+ });
+ } catch (err) {
+ sysError(logger, res, err);
+ }
+});
+
+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/swagger/swagger.yaml b/lib/swagger/swagger.yaml
index e81ae6d..dba5581 100644
--- a/lib/swagger/swagger.yaml
+++ b/lib/swagger/swagger.yaml
@@ -1249,10 +1249,13 @@ paths:
format: uuid
call_hook:
$ref: '#/components/schemas/Webhook'
- description: authentication webhook for inbound calls from PSTN
+ description: application webhook to handle inbound voice calls
call_status_hook:
$ref: '#/components/schemas/Webhook'
- description: webhook for call status events
+ description: webhook to report call status events
+ messaging_hook:
+ $ref: '#/components/schemas/Webhook'
+ description: application webhook to handle inbound SMS/MMS messages
speech_synthesis_vendor:
type: string
speech_synthesis_voice:
@@ -1715,10 +1718,13 @@ components:
format: uuid
call_hook:
$ref: '#/components/schemas/Webhook'
- description: authentication webhook for registration
+ description: application webhook for inbound voice calls
call_status_hook:
$ref: '#/components/schemas/Webhook'
- description: authentication webhook for registration
+ description: webhhok for reporting call status events
+ messaging_hook:
+ $ref: '#/components/schemas/Webhook'
+ description: application webhook for inbound SMS/MMS
speech_synthesis_vendor:
type: string
speech_synthesis_voice:
@@ -1731,8 +1737,6 @@ components:
- application_sid
- name
- account_sid
- - inbound_hook
- - inbound_status_hook
ApiKey:
type: object
properties:
diff --git a/package.json b/package.json
index dc86149..dcc920d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
- "version": "1.1.7",
+ "version": "1.2.0",
"description": "",
"main": "app.js",
"scripts": {
@@ -15,8 +15,8 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
- "@jambonz/db-helpers": "^0.3.8",
- "@jambonz/realtimedb-helpers": "0.2.15",
+ "@jambonz/db-helpers": "^0.5.0",
+ "@jambonz/realtimedb-helpers": "0.2.16",
"cors": "^2.8.5",
"express": "^4.17.1",
"mysql2": "^2.1.0",
@@ -30,7 +30,7 @@
"yamljs": "^0.3.0"
},
"devDependencies": {
- "eslint": "^6.8.0",
+ "eslint": "^7.10.0",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^15.1.0",
"request-promise-native": "^1.0.9",
diff --git a/test/applications.js b/test/applications.js
index 79f7af1..885dee9 100644
--- a/test/applications.js
+++ b/test/applications.js
@@ -38,6 +38,9 @@ test('application tests', async(t) => {
call_status_hook: {
url: 'http://example.com/status',
method: 'POST'
+ },
+ messaging_hook: {
+ url: 'http://example.com/sms'
}
}
});
@@ -58,6 +61,7 @@ test('application tests', async(t) => {
json: true,
});
t.ok(result[0].name === 'daveh' , 'successfully retrieved application by sid');
+ t.ok(result[0].messaging_hook.url === 'http://example.com/sms' , 'successfully retrieved messaging_hook from application');
/* update applications */
result = await request.put(`/Applications/${sid}`, {
@@ -67,11 +71,21 @@ test('application tests', async(t) => {
body: {
call_hook: {
url: 'http://example2.com'
+ },
+ messaging_hook: {
+ url: 'http://example2.com/mms'
}
}
});
t.ok(result.statusCode === 204, 'successfully updated application');
+ /* validate messaging hook was updated */
+ result = await request.get(`/Applications/${sid}`, {
+ auth: authAdmin,
+ json: true,
+ });
+ t.ok(result[0].messaging_hook.url === 'http://example2.com/mms' , 'successfully updated messaging_hook');
+
/* assign phone number to application */
result = await request.put(`/PhoneNumbers/${phone_number_sid}`, {
auth: authAdmin,