initial checkin

This commit is contained in:
Dave Horton
2020-01-07 10:34:03 -05:00
commit 523e2a308b
61 changed files with 7876 additions and 0 deletions

26
lib/tasks/dial.js Normal file
View File

@@ -0,0 +1,26 @@
const Task = require('./task');
const name = 'dial';
class TaskDial extends Task {
constructor(logger, opts) {
super(logger, opts);
this.name = name;
this.headers = this.data.headers || {};
}
static get name() { return name; }
/**
* Reject an incoming call attempt with a provided status code and (optionally) reason
*/
async exec(cs) {
if (!cs.res.finalResponseSent) {
cs.res.send(this.data.status, this.data.reason, {
headers: this.headers
});
}
return false;
}
}
module.exports = TaskDial;

22
lib/tasks/make_task.js Normal file
View File

@@ -0,0 +1,22 @@
const Task = require('./task');
const TaskSipDecline = require('./sip_decline');
const TaskDial = require('./dial');
const errBadInstruction = new Error('invalid instruction payload');
function makeTask(logger, opts) {
if (typeof opts !== 'object' || Array.isArray(opts)) throw errBadInstruction;
const keys = Object.keys(opts);
if (keys.length !== 1) throw errBadInstruction;
const name = keys[0];
const data = opts[name];
Task.validate(name, data);
switch (name) {
case TaskSipDecline.name: return new TaskSipDecline(logger, data);
case TaskDial.name: return new TaskDial(logger, data);
}
// should never reach
throw new Error(`invalid task ${name} (please update specs.json and make_task.js)`);
}
module.exports = makeTask;

26
lib/tasks/sip_decline.js Normal file
View File

@@ -0,0 +1,26 @@
const Task = require('./task');
const name = 'sip:decline';
class TaskSipDecline extends Task {
constructor(logger, opts) {
super(logger, opts);
this.name = name;
this.headers = this.data.headers || {};
}
static get name() { return name; }
/**
* Reject an incoming call attempt with a provided status code and (optionally) reason
*/
async exec(cs) {
if (!cs.res.finalResponseSent) {
cs.res.send(this.data.status, this.data.reason, {
headers: this.headers
});
}
return false;
}
}
module.exports = TaskSipDecline;

97
lib/tasks/specs.json Normal file
View File

@@ -0,0 +1,97 @@
{
"sip:decline": {
"properties": {
"status": "number",
"reason": "string",
"headers": "object"
},
"required": [
"status"
]
},
"dial": {
"properties": {
"action": "string",
"answerOnBridge": "boolean",
"callerId": "string",
"dialMusic": "string",
"method": {
"type": "string",
"enum": ["GET", "POST"]
},
"target": ["#target"],
"timeLimit": "number",
"timeout": "number",
"headers": "object",
"strategy": {
"type": "string",
"enum": ["hunt", "simring"]
},
"transcribe": "#transcribe",
"listen": "#listen"
},
"required": [
"target"
]
},
"listen": {
"properties": {
"metadata": "object",
"mixType": {
"type": "string",
"enum": ["mono", "stereo", "mixed"]
},
"passDtmf": "boolean",
"sampleRate": "number",
"source": {
"type": "string",
"enum": ["parent", "child", "both"]
},
"wsUrl": "string"
},
"required": [
"wsUrl",
"sampleRate"
]
},
"transcribe": {
"properties": {
"action": "string",
"interim": "boolean",
"jsonKey": "string",
"language": "string",
"source": "string",
"vendor": "string"
},
"required": [
"action",
"jsonKey",
"language"
]
},
"target": {
"properties": {
"type": {
"type": "string",
"enum": ["phone", "sip", "user"]
},
"number": "string",
"uri": "string",
"auth": "#auth",
"name": "string"
},
"required": [
"type"
]
},
"auth": {
"properties": {
"user": "string",
"password": "string"
},
"required": [
"user",
"password"
]
}
}

66
lib/tasks/task.js Normal file
View File

@@ -0,0 +1,66 @@
const debug = require('debug')('jambonz:feature-server');
const assert = require('assert');
const specs = new Map();
const _specData = require('./specs');
for (const key in _specData) {specs.set(key, _specData[key]);}
class Task {
constructor(logger, data) {
this.logger = logger;
this.data = data;
}
static validate(name, data) {
debug(`validating ${name} with data ${JSON.stringify(data)}`);
// validate the instruction is supported
if (!specs.has(name)) throw new Error(`invalid instruction: ${name}`);
// check type of each element and make sure required elements are present
const specData = specs.get(name);
let required = specData.required || [];
for (const dKey in data) {
if (dKey in specData.properties) {
const dVal = data[dKey];
const dSpec = specData.properties[dKey];
if (typeof dSpec === 'string' && ['number', 'string', 'object', 'boolean'].includes(dSpec)) {
// simple types
if (typeof dVal !== specData.properties[dKey]) {
throw new Error(`${name}: property ${dKey} has invalid data type`);
}
}
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
const name = dSpec[0].slice(1);
for (const item of dVal) {
Task.validate(name, item);
}
}
else if (typeof dSpec === 'object') {
// complex types
const type = dSpec.type;
assert.ok(['number', 'string', 'object', 'boolean'].includes(type),
`invalid or missing type in spec ${JSON.stringify(dSpec)}`);
if (type === 'string' && dSpec.enum) {
assert.ok(Array.isArray(dSpec.enum), `enum must be an array ${JSON.stringify(dSpec.enum)}`);
if (!dSpec.enum.includes(dVal)) throw new Error(`invalid value ${dVal} must be one of ${dSpec.enum}`);
}
}
else if (typeof dSpec === 'string' && dSpec.startsWith('#')) {
// reference to another datatype (i.e. nested type)
// TODO: validate recursively
const name = dSpec.slice(1);
Task.validate(name, dVal);
}
else {
assert.ok(0, `invalid spec ${JSON.stringify(dSpec)}`);
}
required = required.filter((item) => item !== dKey);
}
else throw new Error(`${name}: unknown property ${dKey}`);
}
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);
}
}
module.exports = Task;