mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-07-23 12:32:06 +00:00
initial changes to support messaging
This commit is contained in:
+4
-1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user