initial changes to support messaging

This commit is contained in:
Dave Horton
2020-09-29 15:45:08 -04:00
parent 491b44709c
commit ee27cf41cb
9 changed files with 166 additions and 23 deletions
+4 -1
View File
@@ -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);
+23 -7
View File
@@ -194,11 +194,13 @@
<SQLField>
<name><![CDATA[expires_at]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<defaultValue><![CDATA[NULL]]></defaultValue>
<uid><![CDATA[DE86BC18-858E-4D7E-9B83-891DB2861434]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[last_used]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<defaultValue><![CDATA[NULL]]></defaultValue>
<uid><![CDATA[11A93288-B892-436B-9BB4-D5C3B70DB061]]></uid>
</SQLField>
<SQLField>
@@ -764,7 +766,7 @@
</location>
<size>
<width>345.00</width>
<height>240.00</height>
<height>260.00</height>
</size>
<zorder>0</zorder>
<SQLField>
@@ -812,7 +814,7 @@
<destinationCardinality>2</destinationCardinality>
<referencesFieldUID><![CDATA[E046BA30-BC18-483C-A5C8-766E7160F574]]></referencesFieldUID>
<referencesTableUID><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></referencesTableUID>
<objectComment><![CDATA[webhook to call for inbound calls to phone numbers owned by this account]]></objectComment>
<objectComment><![CDATA[webhook to call for inbound calls ]]></objectComment>
<uid><![CDATA[55AE0F31-209A-49F9-A6CB-1BB31BE4178A]]></uid>
</SQLField>
<SQLField>
@@ -829,6 +831,20 @@
<objectComment><![CDATA[webhook to call for call status events]]></objectComment>
<uid><![CDATA[A3B4621B-DF13-4920-A718-068B20CB4E8A]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[messaging_hook_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>webhook_sid</referencesField>
<referencesTable>webhooks</referencesTable>
<referencesField><![CDATA[webhook_sid]]></referencesField>
<referencesTable><![CDATA[webhooks]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<referencesFieldUID><![CDATA[E046BA30-BC18-483C-A5C8-766E7160F574]]></referencesFieldUID>
<referencesTableUID><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></referencesTableUID>
<objectComment><![CDATA[webhook to call for inbound SMS/MMS ]]></objectComment>
<uid><![CDATA[4690A98A-2F67-4205-A5C1-D9523F6B3282]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[speech_synthesis_vendor]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
@@ -1083,17 +1099,17 @@
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
<PageGridVisible><![CDATA[0]]></PageGridVisible>
<RightSidebarWidth><![CDATA[1944.000000]]></RightSidebarWidth>
<RightSidebarWidth><![CDATA[1924.000000]]></RightSidebarWidth>
<sidebarIndex><![CDATA[2]]></sidebarIndex>
<snapToGrid><![CDATA[0]]></snapToGrid>
<SourceSidebarWidth><![CDATA[0.000000]]></SourceSidebarWidth>
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
<windowHeight><![CDATA[1194.000000]]></windowHeight>
<windowLocationX><![CDATA[26.000000]]></windowLocationX>
<windowLocationY><![CDATA[100.000000]]></windowLocationY>
<windowHeight><![CDATA[1013.000000]]></windowHeight>
<windowLocationX><![CDATA[2716.000000]]></windowLocationX>
<windowLocationY><![CDATA[1913.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{0, 5}]]></windowScrollOrigin>
<windowWidth><![CDATA[2221.000000]]></windowWidth>
<windowWidth><![CDATA[2201.000000]]></windowWidth>
</SQLDocumentInfo>
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>
+12 -3
View File
@@ -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',
}
];
+2 -2
View File
@@ -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'];
+2
View File
@@ -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;
+95
View File
@@ -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;
+10 -6
View File
@@ -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:
+4 -4
View File
@@ -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",
+14
View File
@@ -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,