mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
initial checkin
This commit is contained in:
26
lib/tasks/dial.js
Normal file
26
lib/tasks/dial.js
Normal 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
22
lib/tasks/make_task.js
Normal 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
26
lib/tasks/sip_decline.js
Normal 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
97
lib/tasks/specs.json
Normal 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
66
lib/tasks/task.js
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user