mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-04-04 06:26:59 +00:00
Compare commits
25 Commits
alpha-2.0
...
v0.4.1-bra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e4654692d | ||
|
|
d255339dac | ||
|
|
c25df2ad7e | ||
|
|
348fa4f654 | ||
|
|
fff52c930c | ||
|
|
950f1c83b7 | ||
|
|
e642e13946 | ||
|
|
8f65b0de2f | ||
|
|
868427216f | ||
|
|
2c8c161954 | ||
|
|
884e63e0ef | ||
|
|
3624b05eb6 | ||
|
|
b739737c29 | ||
|
|
15517828d2 | ||
|
|
490472ca68 | ||
|
|
565ad2948c | ||
|
|
31bed8afbd | ||
|
|
6e78b46674 | ||
|
|
a4bcfca9e6 | ||
|
|
c1112ea477 | ||
|
|
4e4ce0914e | ||
|
|
1dc4728574 | ||
|
|
d7eeb52a84 | ||
|
|
f3fcdfc481 | ||
|
|
cd58d4a4f0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -37,4 +37,5 @@ node_modules
|
||||
|
||||
examples/*
|
||||
|
||||
ecosystem.config.js
|
||||
ecosystem.config.js
|
||||
.vscode
|
||||
|
||||
@@ -26,8 +26,19 @@ router.post('/', async(req, res) => {
|
||||
|
||||
switch (target.type) {
|
||||
case 'phone':
|
||||
case 'teams':
|
||||
uri = `sip:${target.number}@${sbcAddress}`;
|
||||
to = target.number;
|
||||
if ('teams' === target.type) {
|
||||
const {lookupTeamsByAccount} = srf.locals.dbHelpers;
|
||||
const obj = await lookupTeamsByAccount(req.body.account_sid);
|
||||
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
|
||||
Object.assign(opts.headers, {
|
||||
'X-MS-Teams-FQDN': obj.ms_teams_fqdn,
|
||||
'X-MS-Teams-Tenant-FQDN': target.tenant || obj.tenant_fqdn
|
||||
});
|
||||
if (target.vmail === true) uri = `${uri};opaque=app:voicemail`;
|
||||
}
|
||||
break;
|
||||
case 'user':
|
||||
uri = `sip:${target.name}`;
|
||||
@@ -124,12 +135,14 @@ router.post('/', async(req, res) => {
|
||||
if (err instanceof SipError) {
|
||||
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
|
||||
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
|
||||
sipLogger.info(`REST outdial failed with ${err.status}`);
|
||||
cs.emit('callStatusChange', {callStatus, sipStatus: err.status});
|
||||
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
|
||||
else console.log(`REST outdial failed with ${err.status}`);
|
||||
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: err.status});
|
||||
}
|
||||
else {
|
||||
cs.emit('callStatusChange', {callStatus, sipStatus: 500});
|
||||
sipLogger.error({err}, 'REST outdial failed');
|
||||
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: 500});
|
||||
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
|
||||
else console.error(err);
|
||||
}
|
||||
ep.destroy();
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ module.exports = function(srf, logger) {
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info(`Error retrieving or parsing application: ${err.message}`);
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err.message}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const makeTask = require('../tasks/make_task');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const listTaskNames = require('../utils/summarize-tasks');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
||||
|
||||
/**
|
||||
* @classdesc Represents the execution context for a call.
|
||||
@@ -462,11 +463,11 @@ class CallSession extends Emitter {
|
||||
* @param {Task} task - task to be executed
|
||||
*/
|
||||
async _evalEndpointPrecondition(task) {
|
||||
this.logger.debug('_evalEndpointPrecondition');
|
||||
this.logger.debug('CallSession:_evalEndpointPrecondition');
|
||||
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
|
||||
if (this.ep) {
|
||||
if (!task.earlyMedia || this.dlg) return this.ep;
|
||||
if (task.earlyMedia === true || this.dlg) return this.ep;
|
||||
|
||||
// we are going from an early media connection to answer
|
||||
await this.propagateAnswer();
|
||||
@@ -497,8 +498,15 @@ class CallSession extends Emitter {
|
||||
|
||||
return ep;
|
||||
} catch (err) {
|
||||
this.logger.error(err, `Error attempting to allocate endpoint for for task ${task.name}`);
|
||||
throw new Error(`${BADPRECONDITIONS}: unable to allocate endpoint`);
|
||||
if (err === CALLER_CANCELLED_ERR_MSG) {
|
||||
this.logger.error(err, 'caller canceled quickly before we could respond, ending call');
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
|
||||
this._callReleased();
|
||||
}
|
||||
else {
|
||||
this.logger.error(err, `Error attempting to allocate endpoint for for task ${task.name}`);
|
||||
throw new Error(`${BADPRECONDITIONS}: unable to allocate endpoint`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class Conference extends Task {
|
||||
const dlg = cs.dlg;
|
||||
|
||||
// reset answer time if we were transferred from another feature server
|
||||
if (this.this.connectTime) dlg.connectTime = this.connectTime;
|
||||
if (this.connectTime) dlg.connectTime = this.connectTime;
|
||||
|
||||
|
||||
this.ep.on('destroy', this._kicked.bind(this, cs, dlg));
|
||||
|
||||
@@ -37,12 +37,13 @@ function compareTasks(t1, t2) {
|
||||
if (t1.type !== t2.type) return false;
|
||||
switch (t1.type) {
|
||||
case 'phone':
|
||||
return t1.number === t1.number;
|
||||
return t1.number === t2.number;
|
||||
case 'user':
|
||||
return t1.name === t2.name;
|
||||
case 'teams':
|
||||
return t2.name === t1.name;
|
||||
return t1.number === t2.number;
|
||||
case 'sip':
|
||||
return t2.sipUri === t1.sipUri;
|
||||
return t1.sipUri === t2.sipUri;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +84,7 @@ class TaskDial extends Task {
|
||||
this.confirmHook = this.data.confirmHook;
|
||||
this.confirmMethod = this.data.confirmMethod;
|
||||
this.dtmfHook = this.data.dtmfHook;
|
||||
this.proxy = this.data.proxy;
|
||||
|
||||
if (this.dtmfHook) {
|
||||
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
|
||||
@@ -245,7 +247,7 @@ class TaskDial extends Task {
|
||||
const {req, srf} = cs;
|
||||
const {getSBC} = srf.locals;
|
||||
const {lookupTeamsByAccount} = srf.locals.dbHelpers;
|
||||
const sbcAddress = getSBC();
|
||||
const sbcAddress = this.proxy || getSBC();
|
||||
const teamsInfo = {};
|
||||
|
||||
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
||||
@@ -255,10 +257,11 @@ class TaskDial extends Task {
|
||||
callingNumber: this.callerId || req.callingNumber
|
||||
};
|
||||
|
||||
if (this.target.find((t) => t.type === 'teams')) {
|
||||
const t = this.target.find((t) => t.type === 'teams');
|
||||
if (t) {
|
||||
const obj = await lookupTeamsByAccount(cs.accountSid);
|
||||
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
|
||||
Object.assign(teamsInfo, {tenant_fqdn: obj.tenant_fqdn, ms_teams_fqdn: obj.ms_teams_fqdn});
|
||||
Object.assign(teamsInfo, {tenant_fqdn: t.tenant || obj.tenant_fqdn, ms_teams_fqdn: obj.ms_teams_fqdn});
|
||||
}
|
||||
|
||||
const ms = await cs.getMS();
|
||||
|
||||
70
lib/tasks/dialogflow/digit-buffer.js
Normal file
70
lib/tasks/dialogflow/digit-buffer.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const Emitter = require('events');
|
||||
|
||||
/**
|
||||
* A dtmf collector
|
||||
* @class
|
||||
*/
|
||||
class DigitBuffer extends Emitter {
|
||||
/**
|
||||
* Creates a DigitBuffer
|
||||
* @param {*} logger - a pino logger
|
||||
* @param {*} opts - dtmf collection instructions
|
||||
*/
|
||||
constructor(logger, opts) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.minDigits = opts.min || 1;
|
||||
this.maxDigits = opts.max || 99;
|
||||
this.termDigit = opts.term;
|
||||
this.interdigitTimeout = opts.idt || 8000;
|
||||
this.template = opts.template;
|
||||
this.buffer = '';
|
||||
this.logger.debug(`digitbuffer min: ${this.minDigits} max: ${this.maxDigits} term digit: ${this.termDigit}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* process a received dtmf digit
|
||||
* @param {String} a single digit entered by the caller
|
||||
*/
|
||||
process(digit) {
|
||||
this.logger.debug(`digitbuffer process: ${digit}`);
|
||||
if (digit === this.termDigit) return this._fulfill();
|
||||
this.buffer += digit;
|
||||
if (this.buffer.length === this.maxDigits) return this._fulfill();
|
||||
if (this.buffer.length >= this.minDigits) this._startInterDigitTimer();
|
||||
this.logger.debug(`digitbuffer buffer: ${this.buffer}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear the digit buffer
|
||||
*/
|
||||
flush() {
|
||||
if (this.idtimer) clearTimeout(this.idtimer);
|
||||
this.buffer = '';
|
||||
}
|
||||
|
||||
_fulfill() {
|
||||
this.logger.debug(`digit buffer fulfilled with ${this.buffer}`);
|
||||
if (this.template && this.template.includes('${digits}')) {
|
||||
const text = this.template.replace('${digits}', this.buffer);
|
||||
this.logger.info(`reporting dtmf as ${text}`);
|
||||
this.emit('fulfilled', text);
|
||||
}
|
||||
else {
|
||||
this.emit('fulfilled', this.buffer);
|
||||
}
|
||||
this.flush();
|
||||
}
|
||||
|
||||
_startInterDigitTimer() {
|
||||
if (this.idtimer) clearTimeout(this.idtimer);
|
||||
this.idtimer = setTimeout(this._onInterDigitTimeout.bind(this), this.interdigitTimeout);
|
||||
}
|
||||
|
||||
_onInterDigitTimeout() {
|
||||
this.logger.debug('digit buffer timeout');
|
||||
this._fulfill();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DigitBuffer;
|
||||
358
lib/tasks/dialogflow/index.js
Normal file
358
lib/tasks/dialogflow/index.js
Normal file
@@ -0,0 +1,358 @@
|
||||
const Task = require('../task');
|
||||
const {TaskName, TaskPreconditions} = require('../../utils/constants');
|
||||
const Intent = require('./intent');
|
||||
const DigitBuffer = require('./digit-buffer');
|
||||
const Transcription = require('./transcription');
|
||||
const normalizeJambones = require('../../utils/normalize-jambones');
|
||||
|
||||
class Dialogflow extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.credentials = this.data.credentials;
|
||||
this.project = this.data.project;
|
||||
this.lang = this.data.lang || 'en-US';
|
||||
this.welcomeEvent = this.data.welcomeEvent || '';
|
||||
if (this.welcomeEvent.length && this.data.welcomeEventParams && typeof this.data.welcomeEventParams === 'object') {
|
||||
this.welcomeEventParams = this.data.welcomeEventParams;
|
||||
}
|
||||
if (this.data.noInputTimeout) this.noInputTimeout = this.data.noInputTimeout * 1000;
|
||||
else this.noInputTimeout = 20000;
|
||||
this.noInputEvent = this.data.noInputEvent || 'actions_intent_NO_INPUT';
|
||||
this.passDtmfAsInputText = this.passDtmfAsInputText === true;
|
||||
if (this.data.eventHook) this.eventHook = this.data.eventHook;
|
||||
if (this.eventHook && Array.isArray(this.data.events)) {
|
||||
this.events = this.data.events;
|
||||
}
|
||||
else if (this.eventHook) {
|
||||
// send all events by default - except interim transcripts
|
||||
this.events = [
|
||||
'intent',
|
||||
'transcription',
|
||||
'dtmf',
|
||||
'start-play',
|
||||
'stop-play',
|
||||
'no-input'
|
||||
];
|
||||
}
|
||||
else {
|
||||
this.events = [];
|
||||
}
|
||||
if (this.data.actionHook) this.actionHook = this.data.actionHook;
|
||||
if (this.data.thinkingMusic) this.thinkingMusic = this.data.thinkingMusic;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Dialogflow; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
try {
|
||||
await this.init(cs, ep);
|
||||
|
||||
this.logger.debug(`starting dialogflow bot ${this.project}`);
|
||||
|
||||
// kick it off
|
||||
if (this.welcomeEventParams) {
|
||||
this.ep.api('dialogflow_start',
|
||||
`${this.ep.uuid} ${this.project} ${this.lang} ${this.welcomeEvent} '${JSON.stringify(this.welcomeEventParams)}'`);
|
||||
}
|
||||
else if (this.welcomeEvent.length) {
|
||||
this.ep.api('dialogflow_start',
|
||||
`${this.ep.uuid} ${this.project} ${this.lang} ${this.welcomeEvent}`);
|
||||
}
|
||||
else {
|
||||
this.ep.api('dialogflow_start', `${this.ep.uuid} ${this.project} ${this.lang}`);
|
||||
}
|
||||
this.logger.debug(`started dialogflow bot ${this.project}`);
|
||||
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Dialogflow:exec error');
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected) {
|
||||
this.logger.debug('TaskDialogFlow:kill');
|
||||
this.ep.removeCustomEventListener('dialogflow::intent');
|
||||
this.ep.removeCustomEventListener('dialogflow::transcription');
|
||||
this.ep.removeCustomEventListener('dialogflow::audio_provided');
|
||||
this.ep.removeCustomEventListener('dialogflow::end_of_utterance');
|
||||
this.ep.removeCustomEventListener('dialogflow::error');
|
||||
|
||||
this.performAction({dialogflowResult: 'caller hungup'})
|
||||
.catch((err) => this.logger.error({err}, 'dialogflow - error w/ action webook'));
|
||||
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async init(cs, ep) {
|
||||
this.ep = ep;
|
||||
try {
|
||||
this.ep.addCustomEventListener('dialogflow::intent', this._onIntent.bind(this, ep, cs));
|
||||
this.ep.addCustomEventListener('dialogflow::transcription', this._onTranscription.bind(this, ep, cs));
|
||||
this.ep.addCustomEventListener('dialogflow::audio_provided', this._onAudioProvided.bind(this, ep, cs));
|
||||
this.ep.addCustomEventListener('dialogflow::end_of_utterance', this._onEndOfUtterance.bind(this, ep, cs));
|
||||
this.ep.addCustomEventListener('dialogflow::error', this._onError.bind(this, ep, cs));
|
||||
|
||||
const obj = typeof this.credentials === 'string' ? JSON.parse(this.credentials) : this.credentials;
|
||||
const creds = JSON.stringify(obj);
|
||||
await this.ep.set('GOOGLE_APPLICATION_CREDENTIALS', creds);
|
||||
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error setting credentials');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An intent has been returned. Since we are using SINGLE_UTTERANCE on the dialogflow side,
|
||||
* we may get an empty intent, signified by the lack of a 'response_id' attribute.
|
||||
* In such a case, we just start another StreamingIntentDetectionRequest.
|
||||
* @param {*} ep - media server endpoint
|
||||
* @param {*} evt - event data
|
||||
*/
|
||||
_onIntent(ep, cs, evt) {
|
||||
const intent = new Intent(this.logger, evt);
|
||||
|
||||
if (intent.isEmpty) {
|
||||
/**
|
||||
* An empty intent is returned in 3 conditions:
|
||||
* 1. Our no-input timer fired
|
||||
* 2. We collected dtmf that needs to be fed to dialogflow
|
||||
* 3. A normal dialogflow timeout
|
||||
*/
|
||||
if (this.noinput && this.greetingPlayed) {
|
||||
this.logger.info('no input timer fired, reprompting..');
|
||||
this.noinput = false;
|
||||
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang} ${this.noInputEvent}`);
|
||||
}
|
||||
else if (this.dtmfEntry && this.greetingPlayed) {
|
||||
this.logger.info('dtmf detected, reprompting..');
|
||||
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang} none \'${this.dtmfEntry}\'`);
|
||||
this.dtmfEntry = null;
|
||||
}
|
||||
else if (this.greetingPlayed) {
|
||||
this.logger.info('starting another intent');
|
||||
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
|
||||
}
|
||||
else {
|
||||
this.logger.info('got empty intent');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.events.includes('intent')) {
|
||||
this._performHook(cs, this.eventHook, {event: 'intent', data: evt});
|
||||
}
|
||||
|
||||
// clear the no-input timer and the digit buffer
|
||||
this._clearNoinputTimer();
|
||||
if (this.digitBuffer) this.digitBuffer.flush();
|
||||
|
||||
/* hang up (or tranfer call) after playing next audio file? */
|
||||
if (intent.saysEndInteraction) {
|
||||
// if 'end_interaction' is true, end the dialog after playing the final prompt
|
||||
// (or in 1 second if there is no final prompt)
|
||||
this.hangupAfterPlayDone = true;
|
||||
this.waitingForPlayStart = true;
|
||||
setTimeout(() => {
|
||||
if (this.waitingForPlayStart) {
|
||||
this.logger.info('hanging up since intent was marked end interaction');
|
||||
this.performAction({dialogflowResult: 'completed'});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/* collect digits? */
|
||||
else if (intent.saysCollectDtmf || this.enableDtmfAlways) {
|
||||
const opts = Object.assign({
|
||||
idt: this.opts.interDigitTimeout
|
||||
}, intent.dtmfInstructions || {term: '#'});
|
||||
this.digitBuffer = new DigitBuffer(this.logger, opts);
|
||||
this.digitBuffer.once('fulfilled', this._onDtmfEntryComplete.bind(this, ep));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A transcription - either interim or final - has been returned.
|
||||
* If we are doing barge-in based on hotword detection, check for the hotword or phrase.
|
||||
* If we are playing a filler sound, like typing, during the fullfillment phase, start that
|
||||
* if this is a final transcript.
|
||||
* @param {*} ep - media server endpoint
|
||||
* @param {*} evt - event data
|
||||
*/
|
||||
_onTranscription(ep, cs, evt) {
|
||||
const transcription = new Transcription(this.logger, evt);
|
||||
|
||||
if (this.events.includes('transcription') && transcription.isFinal) {
|
||||
this._performHook(cs, this.eventHook, {event: 'transcription', data: evt});
|
||||
}
|
||||
else if (this.events.includes('interim-transcription') && !transcription.isFinal) {
|
||||
this._performHook(cs, this.eventHook, {event: 'transcription', data: evt});
|
||||
}
|
||||
|
||||
// if a final transcription, start a typing sound
|
||||
if (this.thinkingSound > 0 && !transcription.isEmpty && transcription.isFinal &&
|
||||
transcription.confidence > 0.8) {
|
||||
ep.play(this.data.thinkingSound).catch((err) => this.logger.info(err, 'Error playing typing sound'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller has just finished speaking. No action currently taken.
|
||||
* @param {*} evt - event data
|
||||
*/
|
||||
_onEndOfUtterance(cs, evt) {
|
||||
if (this.events.includes('end-utterance')) {
|
||||
this._performHook(cs, this.eventHook, {event: 'end-utterance'});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialogflow has returned an error of some kind.
|
||||
* @param {*} evt - event data
|
||||
*/
|
||||
_onError(ep, cs, evt) {
|
||||
this.logger.error(`got error: ${JSON.stringify(evt)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio has been received from dialogflow and written to a temporary disk file.
|
||||
* Start playing the audio, after killing any filler sound that might be playing.
|
||||
* When the audio completes, start the no-input timer.
|
||||
* @param {*} ep - media server endpoint
|
||||
* @param {*} evt - event data
|
||||
*/
|
||||
async _onAudioProvided(ep, cs, evt) {
|
||||
this.waitingForPlayStart = false;
|
||||
|
||||
// kill filler audio
|
||||
await ep.api('uuid_break', ep.uuid);
|
||||
|
||||
// start a new intent, (we want to continue to listen during the audio playback)
|
||||
// _unless_ we are transferring or ending the session
|
||||
if (/*this.greetingPlayed &&*/ !this.hangupAfterPlayDone) {
|
||||
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
|
||||
}
|
||||
|
||||
this.playInProgress = true;
|
||||
this.curentAudioFile = evt.path;
|
||||
|
||||
this.logger.info(`starting to play ${evt.path}`);
|
||||
if (this.events.includes('start-play')) {
|
||||
this._performHook(cs, this.eventHook, {event: 'start-play', data: {path: evt.path}});
|
||||
}
|
||||
await ep.play(evt.path);
|
||||
if (this.events.includes('stop-play')) {
|
||||
this._performHook(cs, this.eventHook, {event: 'stop-play', data: {path: evt.path}});
|
||||
}
|
||||
this.logger.info(`finished ${evt.path}`);
|
||||
|
||||
if (this.curentAudioFile === evt.path) {
|
||||
this.playInProgress = false;
|
||||
}
|
||||
/*
|
||||
if (!this.inbound && !this.greetingPlayed) {
|
||||
this.logger.info('finished greeting on outbound call, starting new intent');
|
||||
this.ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
|
||||
}
|
||||
*/
|
||||
this.greetingPlayed = true;
|
||||
|
||||
if (this.hangupAfterPlayDone) {
|
||||
this.logger.info('hanging up since intent was marked end interaction and we completed final prompt');
|
||||
this.performAction({dialogflowResult: 'completed'});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
else {
|
||||
// every time we finish playing a prompt, start the no-input timer
|
||||
this._startNoinputTimer(ep, cs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* receive a dmtf entry from the caller.
|
||||
* If we have active dtmf instructions, collect and process accordingly.
|
||||
*/
|
||||
_onDtmf(ep, cs, evt) {
|
||||
if (this.digitBuffer) this.digitBuffer.process(evt.dtmf);
|
||||
if (this.events.includes('dtmf')) {
|
||||
this._performHook(cs, this.eventHook, {event: 'dtmf', data: evt});
|
||||
}
|
||||
}
|
||||
|
||||
_onDtmfEntryComplete(ep, dtmfEntry) {
|
||||
this.logger.info(`collected dtmf entry: ${dtmfEntry}`);
|
||||
this.dtmfEntry = dtmfEntry;
|
||||
this.digitBuffer = null;
|
||||
// if a final transcription, start a typing sound
|
||||
if (this.thinkingSound > 0) {
|
||||
ep.play(this.thinkingSound).catch((err) => this.logger.info(err, 'Error playing typing sound'));
|
||||
}
|
||||
|
||||
// kill the current dialogflow, which will result in us getting an immediate intent
|
||||
ep.api('dialogflow_stop', `${ep.uuid}`)
|
||||
.catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has not provided any input for some time.
|
||||
* Set the 'noinput' member to true and kill the current dialogflow.
|
||||
* This will result in us re-prompting with an event indicating no input.
|
||||
* @param {*} ep
|
||||
*/
|
||||
_onNoInput(ep, cs) {
|
||||
this.noinput = true;
|
||||
|
||||
if (this.events.includes('no-input')) {
|
||||
this._performHook(cs, this.eventHook, {event: 'no-input'});
|
||||
}
|
||||
|
||||
// kill the current dialogflow, which will result in us getting an immediate intent
|
||||
ep.api('dialogflow_stop', `${ep.uuid}`)
|
||||
.catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the no-input timer, if it is running
|
||||
*/
|
||||
_clearNoinputTimer() {
|
||||
if (this.noinputTimer) {
|
||||
clearTimeout(this.noinputTimer);
|
||||
this.noinputTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the no-input timer. The duration is set in the configuration file.
|
||||
* @param {*} ep
|
||||
*/
|
||||
_startNoinputTimer(ep, cs) {
|
||||
if (!this.noInputTimeout) return;
|
||||
this._clearNoinputTimer();
|
||||
this.noinputTimer = setTimeout(this._onNoInput.bind(this, ep, cs), this.noInputTimeout);
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results) {
|
||||
const json = await this.cs.requestor.request(hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('../make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.performAction({dialogflowResult: 'redirect'}, false);
|
||||
cs.replaceApplication(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Dialogflow;
|
||||
89
lib/tasks/dialogflow/intent.js
Normal file
89
lib/tasks/dialogflow/intent.js
Normal file
@@ -0,0 +1,89 @@
|
||||
class Intent {
|
||||
constructor(logger, evt) {
|
||||
this.logger = logger;
|
||||
this.evt = evt;
|
||||
|
||||
this.logger.debug({evt}, 'intent');
|
||||
this.dtmfRequest = checkIntentForDtmfEntry(logger, evt);
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.evt.response_id.length === 0;
|
||||
}
|
||||
|
||||
get fulfillmentText() {
|
||||
return this.evt.query_result.fulfillment_text;
|
||||
}
|
||||
|
||||
get saysEndInteraction() {
|
||||
return this.evt.query_result.intent.end_interaction ;
|
||||
}
|
||||
|
||||
get saysCollectDtmf() {
|
||||
return !!this.dtmfRequest;
|
||||
}
|
||||
|
||||
get dtmfInstructions() {
|
||||
return this.dtmfRequest;
|
||||
}
|
||||
|
||||
get name() {
|
||||
if (!this.isEmpty) return this.evt.query_result.intent.display_name;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
fulfillmentText: this.fulfillmentText
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Intent;
|
||||
|
||||
/**
|
||||
* Parse a returned intent for DTMF entry information
|
||||
* i.e.
|
||||
* allow-dtmf-x-y-z
|
||||
* x = min number of digits
|
||||
* y = optional, max number of digits
|
||||
* z = optional, terminating character
|
||||
* e.g.
|
||||
* allow-dtmf-5 : collect 5 digits
|
||||
* allow-dtmf-1-4 : collect between 1 to 4 (inclusive) digits
|
||||
* allow-dtmf-1-4-# : collect 1-4 digits, terminating if '#' is entered
|
||||
* @param {*} intent - dialogflow intent
|
||||
*/
|
||||
const checkIntentForDtmfEntry = (logger, intent) => {
|
||||
const qr = intent.query_result;
|
||||
if (!qr || !qr.fulfillment_messages || !qr.output_contexts) {
|
||||
logger.info({f: qr.fulfillment_messages, o: qr.output_contexts}, 'no dtmfs');
|
||||
return;
|
||||
}
|
||||
|
||||
// check for custom payloads with a gather verb
|
||||
const custom = qr.fulfillment_messages.find((f) => f.payload && f.payload.verb === 'gather');
|
||||
if (custom && custom.payload && custom.payload.verb === 'gather') {
|
||||
logger.info({custom}, 'found dtmf custom payload');
|
||||
return {
|
||||
max: custom.payload.numDigits,
|
||||
term: custom.payload.finishOnKey,
|
||||
template: custom.payload.responseTemplate
|
||||
};
|
||||
}
|
||||
|
||||
// check for an output context with a specific naming convention
|
||||
const context = qr.output_contexts.find((oc) => oc.name.includes('/contexts/allow-dtmf-'));
|
||||
if (context) {
|
||||
const arr = /allow-dtmf-(\d+)(?:-(\d+))?(?:-(.*))?/.exec(context.name);
|
||||
if (arr) {
|
||||
logger.info({custom}, 'found dtmf output context');
|
||||
return {
|
||||
min: parseInt(arr[1]),
|
||||
max: arr.length > 2 ? parseInt(arr[2]) : null,
|
||||
term: arr.length > 3 ? arr[3] : null
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
41
lib/tasks/dialogflow/transcription.js
Normal file
41
lib/tasks/dialogflow/transcription.js
Normal file
@@ -0,0 +1,41 @@
|
||||
class Transcription {
|
||||
constructor(logger, evt) {
|
||||
this.logger = logger;
|
||||
|
||||
this.recognition_result = evt.recognition_result;
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return !this.recognition_result;
|
||||
}
|
||||
|
||||
get isFinal() {
|
||||
return this.recognition_result && this.recognition_result.is_final === true;
|
||||
}
|
||||
|
||||
get confidence() {
|
||||
if (!this.isEmpty) return this.recognition_result.confidence;
|
||||
}
|
||||
|
||||
get text() {
|
||||
if (!this.isEmpty) return this.recognition_result.transcript;
|
||||
}
|
||||
|
||||
startsWith(str) {
|
||||
return (this.text.toLowerCase() || '').startsWith(str.toLowerCase());
|
||||
}
|
||||
|
||||
includes(str) {
|
||||
return (this.text.toLowerCase() || '').includes(str.toLowerCase());
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
final: this.recognition_result.is_final === true,
|
||||
text: this.text,
|
||||
confidence: this.confidence
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Transcription;
|
||||
@@ -150,8 +150,14 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
async _resolve(reason, evt) {
|
||||
if (this.resolved) return;
|
||||
this.resolved = true;
|
||||
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
|
||||
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.ep.stopTranscription().catch((err) => this.logger.error({err}, 'Error stopping transcription'));
|
||||
}
|
||||
|
||||
this._clearTimer();
|
||||
if (reason.startsWith('dtmf')) {
|
||||
await this.performAction({digits: this.digitBuffer});
|
||||
|
||||
@@ -24,6 +24,9 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.Dial:
|
||||
const TaskDial = require('./dial');
|
||||
return new TaskDial(logger, data, parent);
|
||||
case TaskName.Dialogflow:
|
||||
const TaskDialogflow = require('./dialogflow');
|
||||
return new TaskDialogflow(logger, data, parent);
|
||||
case TaskName.Dequeue:
|
||||
const TaskDequeue = require('./dequeue');
|
||||
return new TaskDequeue(logger, data, parent);
|
||||
|
||||
@@ -20,24 +20,27 @@ class TaskSay extends Task {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
try {
|
||||
const filepath = [];
|
||||
while (!this.killed && this.loop--) {
|
||||
// synthesize all of the text elements
|
||||
const filepath = (await Promise.all(this.text.map(async(text) => {
|
||||
const fp = await synthAudio({
|
||||
text,
|
||||
vendor: this.synthesizer.vendor || cs.speechSynthesisVendor,
|
||||
language: this.synthesizer.language || cs.speechSynthesisLanguage,
|
||||
voice: this.synthesizer.voice || cs.speechSynthesisVoice,
|
||||
salt: cs.callSid
|
||||
}).catch((err) => this.logger.error(err, 'Error synthesizing text'));
|
||||
if (fp) cs.trackTmpFile(fp);
|
||||
return fp;
|
||||
})))
|
||||
.filter((fp) => fp && fp.length);
|
||||
|
||||
this.logger.debug({filepath}, 'synthesized files for tts');
|
||||
|
||||
while (!this.killed && this.loop-- && this.ep.connected) {
|
||||
let segment = 0;
|
||||
do {
|
||||
if (filepath.length <= segment) {
|
||||
const opts = Object.assign({
|
||||
text: this.text[segment],
|
||||
vendor: cs.speechSynthesisVendor,
|
||||
language: cs.speechSynthesisLanguage,
|
||||
voice: cs.speechSynthesisVoice,
|
||||
salt: cs.callSid
|
||||
}, this.synthesizer);
|
||||
const path = await synthAudio(opts);
|
||||
filepath.push(path);
|
||||
cs.trackTmpFile(path);
|
||||
}
|
||||
await ep.play(filepath[segment]);
|
||||
} while (++segment < this.text.length);
|
||||
} while (!this.killed && ++segment < filepath.length);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'TaskSay:exec error');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const {TaskName, TaskPreconditions, CallStatus} = require('../utils/constants');
|
||||
|
||||
/**
|
||||
* Rejects an incoming call with user-specified status code and reason
|
||||
@@ -19,6 +19,7 @@ class TaskSipDecline extends Task {
|
||||
res.send(this.data.status, this.data.reason, {
|
||||
headers: this.headers
|
||||
});
|
||||
cs.emit('callStatusChange', {callStatus: CallStatus.Failed, sipStatus: this.data.status});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,12 +112,34 @@
|
||||
"target": ["#target"],
|
||||
"timeLimit": "number",
|
||||
"timeout": "number",
|
||||
"proxy": "string",
|
||||
"transcribe": "#transcribe"
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
]
|
||||
},
|
||||
"dialogflow": {
|
||||
"properties": {
|
||||
"credentials": "object|string",
|
||||
"project": "string",
|
||||
"lang": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string",
|
||||
"events": "[string]",
|
||||
"welcomeEvent": "string",
|
||||
"welcomeEventParams": "object",
|
||||
"noInputTimeout": "number",
|
||||
"noInputEvent": "string",
|
||||
"passDtmfAsTextInput": "boolean",
|
||||
"thinkingMusic": "string"
|
||||
},
|
||||
"required": [
|
||||
"project",
|
||||
"credentials",
|
||||
"lang"
|
||||
]
|
||||
},
|
||||
"listen": {
|
||||
"properties": {
|
||||
"actionHook": "object|string",
|
||||
@@ -167,6 +189,7 @@
|
||||
"from": "string",
|
||||
"speech_synthesis_vendor": "string",
|
||||
"speech_synthesis_voice": "string",
|
||||
"speech_synthesis_language": "string",
|
||||
"speech_recognizer_vendor": "string",
|
||||
"speech_recognizer_language": "string",
|
||||
"tag": "object",
|
||||
@@ -204,7 +227,7 @@
|
||||
"type": "string",
|
||||
"enum": ["phone", "sip", "user", "teams"]
|
||||
},
|
||||
"url": "string",
|
||||
"confirmHook": "object|string",
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
@@ -213,7 +236,9 @@
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
"auth": "#auth",
|
||||
"vmail": "boolean"
|
||||
"vmail": "boolean",
|
||||
"tenant": "string",
|
||||
"overrideTo": "string"
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"Conference": "conference",
|
||||
"Dequeue": "dequeue",
|
||||
"Dial": "dial",
|
||||
"Dialogflow": "dialogflow",
|
||||
"Enqueue": "enqueue",
|
||||
"Gather": "gather",
|
||||
"Hangup": "hangup",
|
||||
|
||||
@@ -26,7 +26,7 @@ function installSrfLocals(srf, logger) {
|
||||
logger.debug('installing srf locals');
|
||||
assert(!srf.locals.dbHelpers);
|
||||
const {getSBC, lifecycleEmitter} = require('./sbc-pinger')(logger);
|
||||
const StatsCollector = require('jambonz-stats-collector');
|
||||
const StatsCollector = require('@jambonz/stats-collector');
|
||||
const stats = srf.locals.stats = new StatsCollector(logger);
|
||||
|
||||
// freeswitch connections (typically we connect to only one)
|
||||
@@ -130,7 +130,7 @@ function installSrfLocals(srf, logger) {
|
||||
removeFromList,
|
||||
lengthOfList,
|
||||
getListPosition
|
||||
} = require('jambonz-realtimedb-helpers')({
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
|
||||
@@ -11,7 +11,7 @@ const registrar = new Registrar({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
});
|
||||
|
||||
const deepcopy = require('deepcopy');
|
||||
const moment = require('moment');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
@@ -59,6 +59,7 @@ class SingleDialer extends Emitter {
|
||||
|
||||
async exec(srf, ms, opts) {
|
||||
opts = opts || {};
|
||||
opts.headers = opts.headers || {};
|
||||
let uri, to;
|
||||
try {
|
||||
switch (this.target.type) {
|
||||
@@ -69,7 +70,6 @@ class SingleDialer extends Emitter {
|
||||
to = this.target.number;
|
||||
if ('teams' === this.target.type) {
|
||||
assert(this.target.teamsInfo);
|
||||
opts.headers = opts.headers || {};
|
||||
Object.assign(opts.headers, {
|
||||
'X-MS-Teams-FQDN': this.target.teamsInfo.ms_teams_fqdn,
|
||||
'X-MS-Teams-Tenant-FQDN': this.target.teamsInfo.tenant_fqdn
|
||||
@@ -83,6 +83,12 @@ class SingleDialer extends Emitter {
|
||||
uri = `sip:${this.target.name}`;
|
||||
to = this.target.name;
|
||||
|
||||
if (this.target.overrideTo) {
|
||||
Object.assign(opts.headers, {
|
||||
'X-Override-To': this.target.overrideTo
|
||||
});
|
||||
}
|
||||
|
||||
// need to send to the SBC registered on
|
||||
const reg = await registrar.query(aor);
|
||||
if (reg) {
|
||||
@@ -108,13 +114,22 @@ class SingleDialer extends Emitter {
|
||||
|
||||
this.ep = await ms.createEndpoint();
|
||||
this.logger.debug(`SingleDialer:exec - created endpoint ${this.ep.uuid}`);
|
||||
let sdp;
|
||||
|
||||
/**
|
||||
* were we killed whilst we were off getting an endpoint ?
|
||||
* https://github.com/jambonz/jambonz-feature-server/issues/30
|
||||
*/
|
||||
if (this.killed) {
|
||||
this.logger.info('SingleDialer:exec got quick CANCEL from caller, abort outdial');
|
||||
this.ep.destroy()
|
||||
.catch((err) => this.logger.error({err}, 'Error destroying endpoint'));
|
||||
return;
|
||||
}
|
||||
let lastSdp;
|
||||
const connectStream = async(remoteSdp) => {
|
||||
if (remoteSdp !== sdp) {
|
||||
this.ep.modify(sdp = remoteSdp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (remoteSdp === lastSdp) return;
|
||||
lastSdp = remoteSdp;
|
||||
return this.ep.modify(remoteSdp);
|
||||
};
|
||||
|
||||
Object.assign(opts, {
|
||||
@@ -153,16 +168,19 @@ class SingleDialer extends Emitter {
|
||||
cbProvisional: (prov) => {
|
||||
const status = {sipStatus: prov.status};
|
||||
if ([180, 183].includes(prov.status) && prov.body) {
|
||||
status.callStatus = CallStatus.EarlyMedia;
|
||||
if (connectStream(prov.body)) this.emit('earlyMedia');
|
||||
if (status.callStatus !== CallStatus.EarlyMedia) {
|
||||
status.callStatus = CallStatus.EarlyMedia;
|
||||
this.emit('earlyMedia');
|
||||
}
|
||||
connectStream(prov.body);
|
||||
}
|
||||
else status.callStatus = CallStatus.Ringing;
|
||||
this.emit('callStatusChange', status);
|
||||
}
|
||||
});
|
||||
connectStream(this.dlg.remote.sdp);
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.inviteInProgress = null;
|
||||
this.dlg.callSid = this.callSid;
|
||||
await connectStream(this.dlg.remote.sdp);
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`);
|
||||
const connectTime = this.dlg.connectTime = moment();
|
||||
@@ -208,6 +226,7 @@ class SingleDialer extends Emitter {
|
||||
* kill the call in progress or the stable dialog, whichever we have
|
||||
*/
|
||||
async kill() {
|
||||
this.killed = true;
|
||||
if (this.inviteInProgress) await this.inviteInProgress.cancel();
|
||||
else if (this.dlg && this.dlg.connected) {
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
@@ -288,8 +307,9 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
|
||||
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo}) {
|
||||
const sd = new SingleDialer({logger, sbcAddress, target, opts, application, callInfo});
|
||||
sd.exec(srf, ms, opts);
|
||||
const myOpts = deepcopy(opts);
|
||||
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo});
|
||||
sd.exec(srf, ms, myOpts);
|
||||
return sd;
|
||||
}
|
||||
|
||||
|
||||
2017
package-lock.json
generated
2017
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.5",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -26,29 +26,32 @@
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"bent": "^7.0.6",
|
||||
"@jambonz/db-helpers": "^0.4.2",
|
||||
"@jambonz/realtimedb-helpers": "^0.2.16",
|
||||
"@jambonz/stats-collector": "^0.0.4",
|
||||
"aws-sdk": "^2.830.0",
|
||||
"bent": "^7.3.9",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.1.1",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^2.0.1",
|
||||
"drachtio-srf": "^4.4.28",
|
||||
"drachtio-srf": "^4.4.39",
|
||||
"express": "^4.17.1",
|
||||
"ip": "^1.1.5",
|
||||
"@jambonz/db-helpers": "^0.3.7",
|
||||
"jambonz-mw-registrar": "^0.1.3",
|
||||
"jambonz-realtimedb-helpers": "^0.2.13",
|
||||
"jambonz-stats-collector": "^0.0.3",
|
||||
"moment": "^2.24.0",
|
||||
"parse-url": "^5.0.1",
|
||||
"pino": "^5.16.0",
|
||||
"verify-aws-sns-signature": "0.0.5",
|
||||
"moment": "^2.27.0",
|
||||
"parse-url": "^5.0.2",
|
||||
"pino": "^6.5.1",
|
||||
"uuid": "^3.4.0",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"blue-tape": "^1.0.0",
|
||||
"clear-module": "^4.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"nyc": "^15.0.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tap-spec": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('blue-tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
|
||||
|
||||
|
||||
@@ -1,272 +1,257 @@
|
||||
/* SQLEditor (MySQL (2))*/
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS `call_routes`;
|
||||
DROP TABLE IF EXISTS call_routes;
|
||||
|
||||
DROP TABLE IF EXISTS `conference_participants`;
|
||||
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||
|
||||
DROP TABLE IF EXISTS `queue_members`;
|
||||
DROP TABLE IF EXISTS lcr_routes;
|
||||
|
||||
DROP TABLE IF EXISTS `calls`;
|
||||
DROP TABLE IF EXISTS api_keys;
|
||||
|
||||
DROP TABLE IF EXISTS `phone_numbers`;
|
||||
DROP TABLE IF EXISTS ms_teams_tenants;
|
||||
|
||||
DROP TABLE IF EXISTS `applications`;
|
||||
DROP TABLE IF EXISTS sbc_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS `conferences`;
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
DROP TABLE IF EXISTS `queues`;
|
||||
DROP TABLE IF EXISTS phone_numbers;
|
||||
|
||||
DROP TABLE IF EXISTS `subscriptions`;
|
||||
DROP TABLE IF EXISTS sip_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS `registrations`;
|
||||
DROP TABLE IF EXISTS voip_carriers;
|
||||
|
||||
DROP TABLE IF EXISTS `api_keys`;
|
||||
DROP TABLE IF EXISTS accounts;
|
||||
|
||||
DROP TABLE IF EXISTS `accounts`;
|
||||
DROP TABLE IF EXISTS applications;
|
||||
|
||||
DROP TABLE IF EXISTS `service_providers`;
|
||||
DROP TABLE IF EXISTS service_providers;
|
||||
|
||||
DROP TABLE IF EXISTS `sip_gateways`;
|
||||
DROP TABLE IF EXISTS webhooks;
|
||||
|
||||
DROP TABLE IF EXISTS `voip_carriers`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `applications`
|
||||
CREATE TABLE call_routes
|
||||
(
|
||||
`application_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`account_sid` CHAR(36) NOT NULL,
|
||||
`call_hook` VARCHAR(255) NOT NULL,
|
||||
`call_status_hook` VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (`application_sid`)
|
||||
) ENGINE=InnoDB COMMENT='A defined set of behaviors to be applied to phone calls with';
|
||||
call_route_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
priority INTEGER NOT NULL,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
regex VARCHAR(255) NOT NULL,
|
||||
application_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (call_route_sid)
|
||||
) ENGINE=InnoDB COMMENT='a regex-based pattern match for call routing';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `call_routes`
|
||||
CREATE TABLE lcr_routes
|
||||
(
|
||||
`call_route_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`order` INTEGER NOT NULL,
|
||||
`account_sid` CHAR(36) NOT NULL,
|
||||
`regex` VARCHAR(255) NOT NULL,
|
||||
`application_sid` CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (`call_route_sid`)
|
||||
) ENGINE=InnoDB;
|
||||
lcr_route_sid CHAR(36),
|
||||
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
||||
description VARCHAR(1024),
|
||||
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
) COMMENT='Least cost routing table';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `conferences`
|
||||
CREATE TABLE api_keys
|
||||
(
|
||||
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
|
||||
`conference_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`name` VARCHAR(255),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB COMMENT='An audio conference';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `conference_participants`
|
||||
(
|
||||
`conference_participant_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`call_sid` CHAR(36),
|
||||
`conference_sid` CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (`conference_participant_sid`)
|
||||
) ENGINE=InnoDB COMMENT='A relationship between a call and a conference that it is co';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `queues`
|
||||
(
|
||||
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
|
||||
`queue_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`name` VARCHAR(255),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB COMMENT='A set of behaviors to be applied to parked calls';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `registrations`
|
||||
(
|
||||
`registration_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`username` VARCHAR(255) NOT NULL,
|
||||
`domain` VARCHAR(255) NOT NULL,
|
||||
`sip_contact` VARCHAR(255) NOT NULL,
|
||||
`sip_user_agent` VARCHAR(255),
|
||||
PRIMARY KEY (`registration_sid`)
|
||||
) ENGINE=InnoDB COMMENT='An active sip registration';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `queue_members`
|
||||
(
|
||||
`queue_member_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`call_sid` CHAR(36),
|
||||
`queue_sid` CHAR(36) NOT NULL,
|
||||
`position` INTEGER,
|
||||
PRIMARY KEY (`queue_member_sid`)
|
||||
) ENGINE=InnoDB COMMENT='A relationship between a call and a queue that it is waiting';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `calls`
|
||||
(
|
||||
`call_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`parent_call_sid` CHAR(36),
|
||||
`application_sid` CHAR(36),
|
||||
`status_url` VARCHAR(255),
|
||||
`time_start` DATETIME NOT NULL,
|
||||
`time_alerting` DATETIME,
|
||||
`time_answered` DATETIME,
|
||||
`time_ended` DATETIME,
|
||||
`direction` ENUM('inbound','outbound'),
|
||||
`phone_number_sid` CHAR(36),
|
||||
`inbound_user_sid` CHAR(36),
|
||||
`outbound_user_sid` CHAR(36),
|
||||
`calling_number` VARCHAR(255),
|
||||
`called_number` VARCHAR(255),
|
||||
`caller_name` VARCHAR(255),
|
||||
`status` VARCHAR(255) NOT NULL COMMENT 'Possible values are queued, ringing, in-progress, completed, failed, busy and no-answer',
|
||||
`sip_uri` VARCHAR(255) NOT NULL,
|
||||
`sip_call_id` VARCHAR(255) NOT NULL,
|
||||
`sip_cseq` INTEGER NOT NULL,
|
||||
`sip_from_tag` VARCHAR(255) NOT NULL,
|
||||
`sip_via_branch` VARCHAR(255) NOT NULL,
|
||||
`sip_contact` VARCHAR(255),
|
||||
`sip_final_status` INTEGER UNSIGNED,
|
||||
`sdp_offer` VARCHAR(4096),
|
||||
`sdp_answer` VARCHAR(4096),
|
||||
`source_address` VARCHAR(255) NOT NULL,
|
||||
`source_port` INTEGER UNSIGNED NOT NULL,
|
||||
`dest_address` VARCHAR(255),
|
||||
`dest_port` INTEGER UNSIGNED,
|
||||
`url` VARCHAR(255),
|
||||
PRIMARY KEY (`call_sid`)
|
||||
) ENGINE=InnoDB COMMENT='A phone call';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `service_providers`
|
||||
(
|
||||
`service_provider_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`name` VARCHAR(255) NOT NULL UNIQUE ,
|
||||
`description` VARCHAR(255),
|
||||
`root_domain` VARCHAR(255) UNIQUE ,
|
||||
`registration_hook` VARCHAR(255),
|
||||
`hook_basic_auth_user` VARCHAR(255),
|
||||
`hook_basic_auth_password` VARCHAR(255),
|
||||
PRIMARY KEY (`service_provider_sid`)
|
||||
) ENGINE=InnoDB COMMENT='An organization that provides communication services to its ';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `api_keys`
|
||||
(
|
||||
`api_key_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`token` CHAR(36) NOT NULL UNIQUE ,
|
||||
`account_sid` CHAR(36),
|
||||
`service_provider_sid` CHAR(36),
|
||||
PRIMARY KEY (`api_key_sid`)
|
||||
api_key_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
token CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36),
|
||||
service_provider_sid CHAR(36),
|
||||
expires_at TIMESTAMP,
|
||||
PRIMARY KEY (api_key_sid)
|
||||
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `accounts`
|
||||
CREATE TABLE ms_teams_tenants
|
||||
(
|
||||
`account_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`sip_realm` VARCHAR(255) UNIQUE ,
|
||||
`service_provider_sid` CHAR(36) NOT NULL,
|
||||
`registration_hook` VARCHAR(255),
|
||||
`hook_basic_auth_user` VARCHAR(255),
|
||||
`hook_basic_auth_password` VARCHAR(255),
|
||||
`is_active` BOOLEAN NOT NULL DEFAULT true,
|
||||
PRIMARY KEY (`account_sid`)
|
||||
) ENGINE=InnoDB COMMENT='A single end-user of the platform';
|
||||
ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
service_provider_sid CHAR(36) NOT NULL,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
application_sid CHAR(36),
|
||||
tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
|
||||
PRIMARY KEY (ms_teams_tenant_sid)
|
||||
) COMMENT='A Microsoft Teams customer tenant';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `subscriptions`
|
||||
CREATE TABLE sbc_addresses
|
||||
(
|
||||
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
|
||||
`subscription_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`registration_sid` CHAR(36) NOT NULL,
|
||||
`event` VARCHAR(255),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB COMMENT='An active sip subscription';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `voip_carriers`
|
||||
(
|
||||
`voip_carrier_sid` CHAR(36) NOT NULL UNIQUE ,
|
||||
`name` VARCHAR(255) NOT NULL UNIQUE ,
|
||||
`description` VARCHAR(255),
|
||||
PRIMARY KEY (`voip_carrier_sid`)
|
||||
) ENGINE=InnoDB COMMENT='An external organization that can provide sip trunking and D';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `phone_numbers`
|
||||
(
|
||||
`phone_number_sid` CHAR(36) UNIQUE ,
|
||||
`number` VARCHAR(255) NOT NULL UNIQUE ,
|
||||
`voip_carrier_sid` CHAR(36) NOT NULL,
|
||||
`account_sid` CHAR(36),
|
||||
`application_sid` CHAR(36),
|
||||
PRIMARY KEY (`phone_number_sid`)
|
||||
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sip_gateways`
|
||||
(
|
||||
`sip_gateway_sid` CHAR(36),
|
||||
`ipv4` VARCHAR(32) NOT NULL,
|
||||
`port` INTEGER NOT NULL DEFAULT 5060,
|
||||
`inbound` BOOLEAN NOT NULL,
|
||||
`outbound` BOOLEAN NOT NULL,
|
||||
`voip_carrier_sid` CHAR(36) NOT NULL,
|
||||
`is_active` BOOLEAN NOT NULL DEFAULT true,
|
||||
PRIMARY KEY (`sip_gateway_sid`)
|
||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(255) NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 5060,
|
||||
service_provider_sid CHAR(36),
|
||||
PRIMARY KEY (sbc_address_sid)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX `applications_idx_name` ON `applications` (`account_sid`,`name`);
|
||||
CREATE TABLE users
|
||||
(
|
||||
user_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name CHAR(36) NOT NULL UNIQUE ,
|
||||
hashed_password VARCHAR(1024) NOT NULL,
|
||||
salt CHAR(16) NOT NULL,
|
||||
force_change BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
PRIMARY KEY (user_sid)
|
||||
);
|
||||
|
||||
CREATE INDEX `applications_application_sid_idx` ON `applications` (`application_sid`);
|
||||
CREATE INDEX `applications_name_idx` ON `applications` (`name`);
|
||||
CREATE INDEX `applications_account_sid_idx` ON `applications` (`account_sid`);
|
||||
ALTER TABLE `applications` ADD FOREIGN KEY account_sid_idxfk (`account_sid`) REFERENCES `accounts` (`account_sid`);
|
||||
CREATE TABLE voip_carriers
|
||||
(
|
||||
voip_carrier_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL UNIQUE ,
|
||||
description VARCHAR(255),
|
||||
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account',
|
||||
application_sid CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
|
||||
e164_leading_plus BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) ENGINE=InnoDB COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
CREATE INDEX `call_routes_call_route_sid_idx` ON `call_routes` (`call_route_sid`);
|
||||
ALTER TABLE `call_routes` ADD FOREIGN KEY account_sid_idxfk_1 (`account_sid`) REFERENCES `accounts` (`account_sid`);
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
phone_number_sid CHAR(36) UNIQUE ,
|
||||
number VARCHAR(32) NOT NULL UNIQUE ,
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
account_sid CHAR(36),
|
||||
application_sid CHAR(36),
|
||||
PRIMARY KEY (phone_number_sid)
|
||||
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
|
||||
|
||||
ALTER TABLE `call_routes` ADD FOREIGN KEY application_sid_idxfk (`application_sid`) REFERENCES `applications` (`application_sid`);
|
||||
CREATE TABLE webhooks
|
||||
(
|
||||
webhook_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
url VARCHAR(1024) NOT NULL,
|
||||
method ENUM("GET","POST") NOT NULL DEFAULT 'POST',
|
||||
username VARCHAR(255),
|
||||
password VARCHAR(255),
|
||||
PRIMARY KEY (webhook_sid)
|
||||
) COMMENT='An HTTP callback';
|
||||
|
||||
CREATE INDEX `conferences_conference_sid_idx` ON `conferences` (`conference_sid`);
|
||||
CREATE INDEX `conference_participants_conference_participant_sid_idx` ON `conference_participants` (`conference_participant_sid`);
|
||||
ALTER TABLE `conference_participants` ADD FOREIGN KEY call_sid_idxfk (`call_sid`) REFERENCES `calls` (`call_sid`);
|
||||
CREATE TABLE sip_gateways
|
||||
(
|
||||
sip_gateway_sid CHAR(36),
|
||||
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
|
||||
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
|
||||
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
|
||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (sip_gateway_sid)
|
||||
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
||||
|
||||
ALTER TABLE `conference_participants` ADD FOREIGN KEY conference_sid_idxfk (`conference_sid`) REFERENCES `conferences` (`conference_sid`);
|
||||
CREATE TABLE lcr_carrier_set_entry
|
||||
(
|
||||
lcr_carrier_set_entry_sid CHAR(36),
|
||||
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
|
||||
lcr_route_sid CHAR(36) NOT NULL,
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
|
||||
PRIMARY KEY (lcr_carrier_set_entry_sid)
|
||||
) COMMENT='An entry in the LCR routing list';
|
||||
|
||||
CREATE INDEX `queues_queue_sid_idx` ON `queues` (`queue_sid`);
|
||||
CREATE INDEX `registrations_registration_sid_idx` ON `registrations` (`registration_sid`);
|
||||
CREATE INDEX `queue_members_queue_member_sid_idx` ON `queue_members` (`queue_member_sid`);
|
||||
ALTER TABLE `queue_members` ADD FOREIGN KEY call_sid_idxfk_1 (`call_sid`) REFERENCES `calls` (`call_sid`);
|
||||
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_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
PRIMARY KEY (application_sid)
|
||||
) ENGINE=InnoDB COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||
|
||||
ALTER TABLE `queue_members` ADD FOREIGN KEY queue_sid_idxfk (`queue_sid`) REFERENCES `queues` (`queue_sid`);
|
||||
CREATE TABLE service_providers
|
||||
(
|
||||
service_provider_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL UNIQUE ,
|
||||
description VARCHAR(255),
|
||||
root_domain VARCHAR(128) UNIQUE ,
|
||||
registration_hook_sid CHAR(36),
|
||||
ms_teams_fqdn VARCHAR(255),
|
||||
PRIMARY KEY (service_provider_sid)
|
||||
) ENGINE=InnoDB COMMENT='A partition of the platform used by one service provider';
|
||||
|
||||
CREATE INDEX `calls_call_sid_idx` ON `calls` (`call_sid`);
|
||||
ALTER TABLE `calls` ADD FOREIGN KEY parent_call_sid_idxfk (`parent_call_sid`) REFERENCES `calls` (`call_sid`);
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
account_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
sip_realm VARCHAR(132) UNIQUE COMMENT 'sip domain that will be used for devices registering under this account',
|
||||
service_provider_sid CHAR(36) NOT NULL COMMENT 'service provider that owns the customer relationship with this account',
|
||||
registration_hook_sid CHAR(36) COMMENT 'webhook to call when devices underr this account attempt to register',
|
||||
device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
PRIMARY KEY (account_sid)
|
||||
) ENGINE=InnoDB COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
ALTER TABLE `calls` ADD FOREIGN KEY application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`);
|
||||
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX `calls_phone_number_sid_idx` ON `calls` (`phone_number_sid`);
|
||||
ALTER TABLE `calls` ADD FOREIGN KEY phone_number_sid_idxfk (`phone_number_sid`) REFERENCES `phone_numbers` (`phone_number_sid`);
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
ALTER TABLE `calls` ADD FOREIGN KEY inbound_user_sid_idxfk (`inbound_user_sid`) REFERENCES `registrations` (`registration_sid`);
|
||||
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
|
||||
CREATE INDEX account_sid_idx ON api_keys (account_sid);
|
||||
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE `calls` ADD FOREIGN KEY outbound_user_sid_idxfk (`outbound_user_sid`) REFERENCES `registrations` (`registration_sid`);
|
||||
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
|
||||
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX `service_providers_service_provider_sid_idx` ON `service_providers` (`service_provider_sid`);
|
||||
CREATE INDEX `service_providers_name_idx` ON `service_providers` (`name`);
|
||||
CREATE INDEX `service_providers_root_domain_idx` ON `service_providers` (`root_domain`);
|
||||
CREATE INDEX `api_keys_api_key_sid_idx` ON `api_keys` (`api_key_sid`);
|
||||
CREATE INDEX `api_keys_account_sid_idx` ON `api_keys` (`account_sid`);
|
||||
ALTER TABLE `api_keys` ADD FOREIGN KEY account_sid_idxfk_2 (`account_sid`) REFERENCES `accounts` (`account_sid`);
|
||||
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX `api_keys_service_provider_sid_idx` ON `api_keys` (`service_provider_sid`);
|
||||
ALTER TABLE `api_keys` ADD FOREIGN KEY service_provider_sid_idxfk (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX `accounts_account_sid_idx` ON `accounts` (`account_sid`);
|
||||
CREATE INDEX `accounts_name_idx` ON `accounts` (`name`);
|
||||
CREATE INDEX `accounts_sip_realm_idx` ON `accounts` (`sip_realm`);
|
||||
CREATE INDEX `accounts_service_provider_sid_idx` ON `accounts` (`service_provider_sid`);
|
||||
ALTER TABLE `accounts` ADD FOREIGN KEY service_provider_sid_idxfk_1 (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
|
||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
ALTER TABLE `subscriptions` ADD FOREIGN KEY registration_sid_idxfk (`registration_sid`) REFERENCES `registrations` (`registration_sid`);
|
||||
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
|
||||
CREATE INDEX sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port);
|
||||
|
||||
CREATE INDEX `voip_carriers_voip_carrier_sid_idx` ON `voip_carriers` (`voip_carrier_sid`);
|
||||
CREATE INDEX `voip_carriers_name_idx` ON `voip_carriers` (`name`);
|
||||
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`);
|
||||
CREATE INDEX `phone_numbers_voip_carrier_sid_idx` ON `phone_numbers` (`voip_carrier_sid`);
|
||||
ALTER TABLE `phone_numbers` ADD FOREIGN KEY voip_carrier_sid_idxfk (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
|
||||
CREATE INDEX sbc_address_sid_idx ON sbc_addresses (sbc_address_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON sbc_addresses (service_provider_sid);
|
||||
ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`);
|
||||
CREATE INDEX user_sid_idx ON users (user_sid);
|
||||
CREATE INDEX name_idx ON users (name);
|
||||
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
|
||||
CREATE INDEX name_idx ON voip_carriers (name);
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_sid_idxfk_2 (`application_sid`) REFERENCES `applications` (`application_sid`);
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE UNIQUE INDEX `sip_gateways_sip_gateway_idx_hostport` ON `sip_gateways` (`ipv4`,`port`);
|
||||
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
ALTER TABLE `sip_gateways` ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
|
||||
CREATE UNIQUE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||
|
||||
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid);
|
||||
|
||||
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
|
||||
|
||||
CREATE INDEX application_sid_idx ON applications (application_sid);
|
||||
CREATE INDEX account_sid_idx ON applications (account_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
ALTER TABLE applications ADD FOREIGN KEY call_status_hook_sid_idxfk (call_status_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);
|
||||
ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (registration_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
CREATE INDEX account_sid_idx ON accounts (account_sid);
|
||||
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
|
||||
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
|
||||
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('blue-tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
const async = require('async');
|
||||
|
||||
test('starting docker network..', (t) => {
|
||||
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('blue-tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
|
||||
test('stopping docker network..', (t) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape').test ;
|
||||
const test = require('blue-tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('tape');
|
||||
const test = require('blue-tape');
|
||||
const debug = require('debug')('drachtio:jambonz:test');
|
||||
const makeTask = require('../lib/tasks/make_task');
|
||||
const noop = () => {};
|
||||
|
||||
Reference in New Issue
Block a user