Compare commits

..

34 Commits

Author SHA1 Message Date
Dave Horton
1e93973419 Feature/azure recognition (#46)
* add support for microsoft speech recognition

* update to drachtio-fsmrf that support microsoft stt

* gather and transcribe now support microsoft
2021-11-26 16:40:25 -06:00
Dave Horton
fe1778e9ae Feature/sip refer (#44)
* changes to support sip REFER

* implement actionhook

* changes from testing

* minor logging
2021-11-20 11:39:10 -05:00
Dave Horton
af15449451 fix tests 2021-11-19 14:17:10 -05:00
Dave Horton
12c34de15c changes for azure tts 2021-11-19 18:28:42 +00:00
Dave Horton
7c77bedd15 linting 2021-11-19 10:25:11 -05:00
Dave Horton
0c5150cb30 add support for recording conference to a file 2021-11-19 10:07:43 -05:00
Dave Horton
2262973f43 bugfix #41: error was thrown about missing speech creds when speech was not enabled 2021-11-16 19:42:16 -05:00
Dave Horton
db78ffffed dial: make sure to clear max call timer when dial ends 2021-11-15 12:00:48 -05:00
Dave Horton
2930cd6aaf Dockerfile 2021-11-04 12:57:56 -04:00
Dave Horton
2a013377cc update to aes-256-cbc algorithm for encryption 2021-11-03 16:17:20 -04:00
Dave Horton
dcf27ba5d3 trim sensitive info from logs 2021-11-03 14:37:57 -04:00
Dave Horton
f11feb7975 version bump 2021-11-03 13:49:35 -04:00
Dave Horton
19dda9398d bump version 2021-10-21 13:08:45 -04:00
Dave Horton
81edf1a6d6 bump version 2021-10-21 13:00:29 -04:00
Dave Horton
72345f83c1 Feature/minimal media anchoring (#36)
* initial WIP to remove freeswitch from media path when not recording or transcribing dial calls

* implement release-media and anchor-media operations

* mute/unmute now handled by rtpengine

* Dial: dtmf detection now based on SIP INFO events from sbcs and rtpengine

* add reason to gather action, bugfixes for transcribe and say
2021-10-21 11:59:45 -04:00
Dave Horton
bedf25c6a2 update to latest realtimedb 2021-10-02 14:19:05 -04:00
Dave Horton
a9e789f466 add support for autoscaling SBC SIP servers; bugfix: synthAudio calls must past stats obj 2021-10-02 12:40:56 -04:00
Dave Horton
a779ead79f minor fix for gather 2021-09-29 18:15:52 -04:00
Dave Horton
a3d3878218 bugfix: cs not passed to kill() 2021-09-28 09:58:59 -04:00
Dave Horton
4bc3e03605 bugfix: 302 response in rest outdial caused restart 2021-09-27 10:39:17 -04:00
Dave Horton
62106a751f fix bug in createCall 2021-09-27 08:41:45 -04:00
Dave Horton
4c61ae5fbd add support for conference members joining in a muted state 2021-09-25 13:50:16 -04:00
Dave Horton
708c13d5f6 add support for muting/unmuting non moderators in a conference 2021-09-25 12:31:20 -04:00
Dave Horton
7cf342eeb8 add support for overrideTo and 302 redirect on rest outdial 2021-09-24 09:58:39 -04:00
Dave Horton
aebcf2b006 say now supports loop="forever" 2021-09-24 07:01:26 -04:00
Dave Horton
f0bd681ccc implement actionHook for message verb 2021-09-22 13:28:56 -04:00
Dave Horton
ac263de729 fix error responses for sms 2021-09-22 10:54:36 -04:00
Dave Horton
862405c232 LCC: add conference hold and unhold actions 2021-09-22 07:39:44 -04:00
Dave Horton
3cd4c399d4 LCC: add support for conf_hold_status to hold/unhold a participant in a conference 2021-09-20 15:50:00 -04:00
Dave Horton
0d6cb8a2b3 bugfix: establish conference start time for parties that have been waiting 2021-09-16 13:08:15 -04:00
Dave Horton
05c5319cbc minor rasa fix 2021-09-07 13:43:40 -04:00
Dave Horton
d15fdcf663 rasa: add support for eventhook which provides user and bot messages in realtime and supports redirecting to a new app 2021-09-07 13:43:40 -04:00
Dave Horton
19f3cbaa43 initial support for Rasa 2021-09-07 13:43:40 -04:00
Dave Horton
ac8827c885 dialogflow: support for regional endpoints 2021-09-07 13:43:40 -04:00
30 changed files with 2115 additions and 745 deletions

View File

@@ -1,16 +1,10 @@
FROM node:alpine as builder
RUN apk update && apk add --no-cache python make g++
FROM node:16
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
FROM node:alpine as app
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "npm", "start" ]
CMD [ "npm", "start" ]

View File

@@ -57,6 +57,11 @@ router.post('/', async(req, res) => {
case 'user':
uri = `sip:${target.name}`;
to = target.name;
if (target.overrideTo) {
Object.assign(opts.headers, {
'X-Override-To': target.overrideTo
});
}
break;
case 'sip':
uri = target.sipUri;
@@ -105,8 +110,11 @@ router.post('/', async(req, res) => {
/* now launch the outdial */
try {
const dlg = await srf.createUAC(uri, opts, {
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
cbRequest: (err, inviteReq) => {
/* in case of 302 redirect, this gets called twice, ignore the second */
if (res.headersSent) return;
if (err) {
logger.error(err, 'createCall Error creating call');
res.status(500).send('Call Failure');

View File

@@ -137,7 +137,9 @@ module.exports = function(srf, logger) {
const obj = Object.assign({}, app);
delete obj.requestor;
delete obj.notifier;
logger.info({app: obj}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
// eslint-disable-next-line no-unused-vars
const {call_hook, call_status_hook, ...appInfo} = obj; // mask sensitive data like user/pass on webhook
logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
req.locals.callInfo = new CallInfo({req, app, direction: CallDirection.Inbound});
next();
} catch (err) {
@@ -167,7 +169,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}});
}
}

View File

@@ -8,13 +8,14 @@ const CallSession = require('./call-session');
*/
class AdultingCallSession extends CallSession {
constructor({logger, application, singleDialer, tasks, callInfo}) {
constructor({logger, application, singleDialer, tasks, callInfo, accountInfo}) {
super({
logger,
application,
srf: singleDialer.dlg.srf,
tasks,
callInfo
callInfo,
accountInfo
});
this.sd = singleDialer;

View File

@@ -34,7 +34,7 @@ class CallSession extends Emitter {
* @param {array} opts.tasks - tasks we are to execute
* @param {callInfo} opts.callInfo - information about the call
*/
constructor({logger, application, srf, tasks, callInfo, accountInfo}) {
constructor({logger, application, srf, tasks, callInfo, accountInfo, memberId, confName, confUuid}) {
super();
this.logger = logger;
this.application = application;
@@ -42,6 +42,9 @@ class CallSession extends Emitter {
this.callInfo = callInfo;
this.accountInfo = accountInfo;
this.tasks = tasks;
this.memberId = memberId;
this.confName = confName;
this.confUuid = confUuid;
this.taskIdx = 0;
this.stackIdx = 0;
this.callGone = false;
@@ -196,6 +199,27 @@ class CallSession extends Emitter {
return this.accountInfo?.account?.webhook_secret;
}
get isInConference() {
return this.memberId && this.confName && this.confUuid;
}
setConferenceDetails(memberId, confName, confUuid) {
assert(!this.memberId && !this.confName && !this.confUuid);
assert (memberId && confName && confUuid);
this.logger.debug(`session is now in conference ${confName}:${memberId} - uuid ${confUuid}`);
this.memberId = memberId;
this.confName = confName;
this.confUuid = confUuid;
}
clearConferenceDetails() {
this.logger.debug(`session has now left conference ${this.confName}:${this.memberId}`);
this.memberId = null;
this.confName = null;
this.confUuid = null;
}
/**
* Check for speech credentials for the specified vendor
* @param {*} vendor - google or aws
@@ -233,6 +257,13 @@ class CallSession extends Emitter {
region: process.env.AWS_REGION || credential.aws_region
};
}
else if ('microsoft' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key,
region: credential.region
};
}
}
else {
writeAlerts({
@@ -339,7 +370,7 @@ class CallSession extends Emitter {
*/
_lccCallStatus(opts) {
if (opts.call_status === CallStatus.Completed && this.dlg) {
this.logger.info('CallSession:updateCall hanging up call due to request from api');
this.logger.info('CallSession:_lccCallStatus hanging up call due to request from api');
this._callerHungup();
}
else if (opts.call_status === CallStatus.NoAnswer) {
@@ -388,7 +419,7 @@ class CallSession extends Emitter {
const {parentLogger} = this.srf.locals;
const childLogger = parentLogger.child({callId: this.callId, callSid: sd.callSid});
const t = normalizeJambones(childLogger, childTasks).map((tdata) => makeTask(childLogger, tdata));
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list for child call');
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list for child call');
const cs = await sd.doAdulting({
logger: childLogger,
application: this.application,
@@ -400,7 +431,7 @@ class CallSession extends Emitter {
}
if (tasks) {
const t = normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata));
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list');
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list');
this.replaceApplication(t);
}
else {
@@ -417,23 +448,39 @@ class CallSession extends Emitter {
async _lccListenStatus(opts) {
const task = this.currentTask;
if (!task || ![TaskName.Dial, TaskName.Listen].includes(task.name)) {
return this.logger.info(`CallSession:updateCall - invalid listen_status in task ${task.name}`);
return this.logger.info(`CallSession:_lccListenStatus - invalid listen_status in task ${task.name}`);
}
const listenTask = task.name === TaskName.Listen ? task : task.listenTask;
if (!listenTask) {
return this.logger.info('CallSession:updateCall - invalid listen_status: Dial does not have a listen');
return this.logger.info('CallSession:_lccListenStatus - invalid listen_status: Dial does not have a listen');
}
listenTask.updateListen(opts.listen_status);
}
async _lccMuteStatus(callSid, mute) {
// this whole thing requires us to be in a Dial verb
// this whole thing requires us to be in a Dial or Conference verb
const task = this.currentTask;
if (!task || TaskName.Dial !== task.name) {
return this.logger.info('CallSession:_lccMute - invalid command as dial is not active');
if (!task || ![TaskName.Dial, TaskName.Conference].includes(task.name)) {
return this.logger.info('CallSession:_lccMuteStatus - invalid: neither dial nor conference are not active');
}
// now do the whisper
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
// now do the mute/unmute
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
}
async _lccConfHoldStatus(callSid, opts) {
const task = this.currentTask;
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
}
task.doConferenceHold(this, opts);
}
async _lccConfMuteStatus(callSid, opts) {
const task = this.currentTask;
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
}
task.doConferenceMuteNonModerators(this, opts);
}
/**
@@ -484,20 +531,6 @@ class CallSession extends Emitter {
task.whisper(tasks, callSid).catch((err) => this.logger.error(err, 'CallSession:_lccWhisper'));
}
/**
* perform live call control -- mute or unmute an endpoint
* @param {array} opts - array of play or say tasks
*/
async _lccMute(callSid, mute) {
// this whole thing requires us to be in a Dial verb
const task = this.currentTask;
if (!task || TaskName.Dial !== task.name) {
return this.logger.info('CallSession:_lccMute - not possible since we are not in a dial');
}
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
}
/**
* perform live call control
@@ -519,6 +552,12 @@ class CallSession extends Emitter {
else if (opts.mute_status) {
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
}
else if (opts.conf_hold_status) {
await this._lccConfHoldStatus(callSid, opts);
}
else if (opts.conf_mute_status) {
await this._lccConfMuteStatus(callSid, opts);
}
// whisper may be the only thing we are asked to do, or it may that
// we are doing a whisper after having muted, paused reccording etc..
@@ -894,6 +933,28 @@ class CallSession extends Emitter {
};
}
async releaseMediaToSBC(remoteSdp) {
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
await this.dlg.modify(remoteSdp, {
headers: {
'X-Reason': 'release-media'
}
});
this.ep.destroy()
.then(() => this.ep = null)
.catch((err) => this.logger.error({err}, 'CallSession:releaseMediaToSBC: Error destroying endpoint'));
}
async reAnchorMedia() {
assert(this.dlg && this.dlg.connected && !this.ep);
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
await this.dlg.modify(this.ep.local.sdp, {
headers: {
'X-Reason': 'anchor-media'
}
});
}
/**
* Called any time call status changes. This method both invokes the
* call_status_hook callback as well as updates the realtime database

View File

@@ -8,7 +8,7 @@ const CallSession = require('./call-session');
*/
class ConfirmCallSession extends CallSession {
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo}) {
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName}) {
super({
logger,
application,
@@ -16,7 +16,9 @@ class ConfirmCallSession extends CallSession {
callSid: dlg.callSid,
tasks,
callInfo,
accountInfo
accountInfo,
memberId,
confName
});
this.dlg = dlg;
this.ep = ep;

View File

@@ -10,6 +10,7 @@ const WAIT = 'wait';
const JOIN = 'join';
const START = 'start';
function confNoMatch(str) {
return str.match(/^No active conferences/) || str.match(/Conference.*not found/);
}
@@ -27,7 +28,8 @@ function camelize(str) {
function unhandled(logger, cs, evt) {
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
logger.debug({evt}, `unhandled conference event: ${evt.getHeader('Action')}`) ;
// logger.debug({evt}, `unhandled conference event: ${evt.getHeader('Action')}`) ;
logger.debug(`unhandled conference event: ${evt.getHeader('Action')}`) ;
}
function capitalize(s) {
@@ -45,10 +47,10 @@ class Conference extends Task {
this.confName = this.data.name;
[
'beep', 'startConferenceOnEnter', 'endConferenceOnExit',
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
].forEach((attr) => this[attr] = this.data[attr]);
this.record = this.data.record || {};
this.statusEvents = [];
if (this.statusHook) {
['start', 'end', 'join', 'leave', 'start-talking', 'stop-talking'].forEach((e) => {
@@ -67,6 +69,9 @@ class Conference extends Task {
get name() { return TaskName.Conference; }
get shouldRecord() { return this.record.path; }
get isRecording() { return this.recordingInProgress; }
async exec(cs, ep) {
await super.exec(cs);
this.ep = ep;
@@ -213,6 +218,7 @@ class Conference extends Task {
this._playSession.kill();
this._playSession = null;
}
cs.clearConferenceDetails();
resolve();
});
@@ -330,15 +336,30 @@ class Conference extends Task {
const opts = {};
if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
if (this.startConferenceOnEnter) Object.assign(opts, {flags: {moderator: true}});
if (this.joinMuted) Object.assign(opts, {flags: {mute: true}});
try {
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
this.memberId = memberId;
this.confUuid = confUuid;
cs.setConferenceDetails(memberId, this.confName, confUuid);
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
this._notifyConferenceEvent(cs, 'join');
// start recording if requested and we just started the conference
if (startConf && this.shouldRecord) {
this.logger.info(`recording conference to ${this.record.path}`);
try {
await this.ep.api(`conference ${this.confName} record ${this.record.path}`);
} catch (err) {
this.logger.info({err}, 'Conference:_joinConference - failed to start recording');
}
}
// listen for conference events
this.ep.filter('Conference-Unique-ID', this.confUuid);
this.ep.conn.on('esl::event::CUSTOM::*', this.__onConferenceEvent.bind(this, cs)) ;
@@ -371,9 +392,70 @@ class Conference extends Task {
*/
notifyStartConference(cs, opts) {
this.logger.info({opts}, `Conference:notifyStartConference: conference ${this.confName} has now started`);
this.conferenceStartTime = new Date();
this.emitter.emit('join', opts);
}
async doConferenceMuteNonModerators(cs, opts) {
const mute = opts.conf_mute_status === 'mute';
assert (cs.isInConference);
this.logger.info(`Conference:doConferenceMuteNonModerators ${mute ? 'muting' : 'unmuting'} non-moderators`);
this.ep.api(`conference ${this.confName} ${mute ? 'mute' : 'unmute'} non_moderator`)
.catch((err) => this.logger.info({err}, 'Error muting or unmuting non_moderators'));
if (this.conf_hold_status !== 'hold' && this._playSession) {
this._playSession.kill();
this._playSession = null;
}
}
async doConferenceHold(cs, opts) {
assert (cs.isInConference);
const {conf_hold_status, wait_hook} = opts;
let hookOnly = true;
if (this.conf_hold_status !== conf_hold_status) {
hookOnly = false;
this.conf_hold_status = conf_hold_status;
const hold = conf_hold_status === 'hold';
this.ep.api(`conference ${this.confName} ${hold ? 'mute' : 'unmute'} ${this.memberId}`)
.catch((err) => this.logger.info({err}, 'Error muting or unmuting participant'));
this.ep.api(`conference ${this.confName} ${hold ? 'deaf' : 'undeaf'} ${this.memberId}`)
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
}
if (hookOnly && this._playSession) {
this._playSession.kill();
this._playSession = null;
}
if (wait_hook && this.conf_hold_status === 'hold') {
const {dlg} = cs;
this._doWaitHookWhileOnHold(cs, dlg, wait_hook);
}
else if (this.conf_hold_status !== 'hold' && this._playSession) {
this._playSession.kill();
this._playSession = null;
}
}
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
do {
try {
const tasks = await this._playHook(cs, dlg, wait_hook);
if (0 === tasks.length) break;
} catch (err) {
if (!this.killed) {
this.logger.info(err, `Conference:_doWait: failed retrieving wait_hook for ${this.confName}`);
}
this._playSession = null;
break;
}
} while (!this.killed && !this.conf_hold_status === 'hold');
}
/**
* Add ourselves to the waitlist of sessions to be notified once
* the conference starts
@@ -464,6 +546,9 @@ class Conference extends Task {
dlg,
ep: cs.ep,
callInfo: cs.callInfo,
accountInfo: cs.accountInfo,
memberId: this.memberId,
confName: this.confName,
tasks
});
await this._playSession.exec();
@@ -484,6 +569,7 @@ class Conference extends Task {
}
async replaceEndpointAndEnd(cs) {
cs.clearConferenceDetails();
if (this.replaced) return;
this.replaced = true;
try {

View File

@@ -130,6 +130,10 @@ class TaskDial extends Task {
get name() { return TaskName.Dial; }
get canReleaseMedia() {
return !process.env.ANCHOR_MEDIA_ALWAYS && !this.listenTask && !this.transcribeTask;
}
async exec(cs) {
await super.exec(cs);
try {
@@ -142,13 +146,12 @@ class TaskDial extends Task {
this.epOther.play(this.dialMusic).catch((err) => {});
}
}
if (this.epOther) this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
await this._attemptCalls(cs);
await this.awaitTaskDone();
this.logger.debug({callSid: this.cs.callSid}, 'Dial:exec task is done, sending actionHook if any');
await this.performAction(this.results, this.killReason !== KillReason.Replaced);
this._removeDtmfDetection(cs, this.epOther);
this._removeDtmfDetection(cs, this.ep);
this._removeDtmfDetection(cs.dlg);
this._removeDtmfDetection(this.dlg);
} catch (err) {
this.logger.error({err}, 'TaskDial:exec terminating with error');
this.kill(cs);
@@ -158,8 +161,12 @@ class TaskDial extends Task {
async kill(cs, reason) {
super.kill(cs);
this.killReason = reason || KillReason.Hangup;
this._removeDtmfDetection(this.cs, this.epOther);
this._removeDtmfDetection(this.cs, this.ep);
if (this.timerMaxCallDuration) {
clearTimeout(this.timerMaxCallDuration);
this.timerMaxCallDuration = null;
}
this._removeDtmfDetection(cs.dlg);
this._removeDtmfDetection(this.dlg);
this._killOutdials();
if (this.sd) {
this.sd.kill();
@@ -168,7 +175,6 @@ class TaskDial extends Task {
if (this.callSid) sessionTracker.remove(this.callSid);
if (this.listenTask) await this.listenTask.kill(cs);
if (this.transcribeTask) await this.transcribeTask.kill(cs);
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
this.notifyTaskDone();
}
@@ -177,9 +183,14 @@ class TaskDial extends Task {
* @param {*} tasks - array of play/say tasks to execute
*/
async whisper(tasks, callSid) {
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
try {
const cs = this.callSession;
if (!this.ep && !this.epOther) {
await this.reAnchorMedia(this.callSession, this.sd);
}
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
this.logger.debug('Dial:whisper unbridging endpoints');
await this.epOther.unbridge();
this.logger.debug('Dial:whisper executing tasks');
@@ -188,7 +199,12 @@ class TaskDial extends Task {
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
}
this.logger.debug('Dial:whisper tasks complete');
if (!cs.callGone && this.epOther) this.epOther.bridge(this.ep);
if (!cs.callGone && this.epOther) {
/* if we can release the media back to the SBC, do so now */
if (this.canReleaseMedia) this._releaseMedia(cs, this.sd);
else this.epOther.bridge(this.ep);
}
} catch (err) {
this.logger.error(err, 'Dial:whisper error');
}
@@ -198,14 +214,19 @@ class TaskDial extends Task {
* mute or unmute one side of the call
*/
async mute(callSid, doMute) {
if (!this.epOther || !this.ep) return this.logger.info('Dial:mute: no paired endpoint found');
const parentCall = callSid !== this.callSid;
const dlg = parentCall ? this.callSession.dlg : this.dlg;
const hdr = `${doMute ? 'mute' : 'unmute'} call leg`;
try {
const parentCall = callSid !== this.callSid;
const ep = parentCall ? this.epOther : this.ep;
await ep[doMute ? 'mute' : 'unmute']();
this.logger.debug(`Dial:mute ${doMute ? 'muted' : 'unmuted'} ${parentCall ? 'parentCall' : 'childCall'}`);
/* let rtpengine do the mute / unmute */
await dlg.request({
method: 'INFO',
headers: {
'X-Reason': hdr
}
});
} catch (err) {
this.logger.error(err, 'Dial:mute error');
this.logger.info({err}, `Dial:mute - ${hdr} error`);
}
}
@@ -217,35 +238,39 @@ class TaskDial extends Task {
this.dials.clear();
}
_installDtmfDetection(cs, ep, dtmfDetector) {
if (ep && this.dtmfHook && !ep.dtmfDetector) {
ep.dtmfDetector = dtmfDetector;
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
}
_installDtmfDetection(cs, dlg) {
dlg.on('info', this._onInfo.bind(this, cs, dlg));
}
_removeDtmfDetection(cs, ep) {
if (ep) {
delete ep.dtmfDetector;
ep.removeAllListeners('dtmf');
}
_removeDtmfDetection(dlg) {
dlg && dlg.removeAllListeners('info');
}
_onDtmf(cs, ep, evt) {
if (ep.dtmfDetector) {
const match = ep.dtmfDetector.keyPress(evt.dtmf);
if (match) {
this.logger.debug({callSid: this.cs.callSid}, `Dial:_onDtmf triggered dtmf match: ${match}`);
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
cs.requestor :
(this.sd ? this.sd.requestor : null);
if (!requestor) {
this.logger.info(`Dial:_onDtmf got digits on B leg after adulting: ${evt.dtmf}`);
}
else {
requestor.request(this.dtmfHook, {dtmf: match, ...cs.callInfo.toJSON()})
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
}
}
_onInfo(cs, dlg, req, res) {
res.send(200);
if (req.get('Content-Type') !== 'application/dtmf-relay') return;
const dtmfDetector = dlg === cs.dlg ? this.parentDtmfCollector : this.childDtmfCollector;
if (!dtmfDetector) return;
let requestor, callSid, callInfo;
if (dtmfDetector === this.parentDtmfCollector) {
requestor = cs.requestor;
callSid = cs.callSid;
callInfo = cs.callInfo;
}
else {
requestor = this.sd?.requestor;
callSid = this.sd?.callSid;
callInfo = this.sd?.callInfo;
}
if (!requestor) return;
const arr = /Signal=([0-9#*])/.exec(req.body);
if (!arr) return;
const key = arr[1];
const match = dtmfDetector.keyPress(key);
if (match) {
this.logger.info({callSid}, `Dial:_onInfo triggered dtmf match: ${match}`);
requestor.request(this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
}
}
@@ -255,7 +280,7 @@ class TaskDial extends Task {
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
/* send outbound legs back to the same SBC (to support static IP feature) */
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port};transport=tcp`;
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port}`;
if (this.dialMusic) {
// play dial music to caller while we outdial
@@ -322,7 +347,8 @@ class TaskDial extends Task {
sbcAddress,
target: t,
opts,
callInfo: cs.callInfo
callInfo: cs.callInfo,
accountInfo: cs.accountInfo
});
this.dials.set(sd.callSid, sd);
@@ -366,9 +392,13 @@ class TaskDial extends Task {
break;
}
})
.on('accept', () => {
.on('accept', async() => {
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
this._connectSingleDial(cs, sd);
try {
await this._connectSingleDial(cs, sd);
} catch (err) {
this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial ');
}
})
.on('decline', () => {
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
@@ -394,8 +424,8 @@ class TaskDial extends Task {
});
}
_connectSingleDial(cs, sd) {
if (!this.bridged) {
async _connectSingleDial(cs, sd) {
if (!this.bridged && !this.canReleaseMedia) {
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
if (this.epOther) {
this.epOther.api('uuid_break', this.epOther.uuid);
@@ -405,7 +435,7 @@ class TaskDial extends Task {
}
// ding! ding! ding! we have a winner
this._selectSingleDial(cs, sd);
await this._selectSingleDial(cs, sd);
this._killOutdials(); // NB: order is important
}
@@ -418,7 +448,7 @@ class TaskDial extends Task {
* - launch any nested tasks
* - and establish a handler to clean up if the called party hangs up
*/
_selectSingleDial(cs, sd) {
async _selectSingleDial(cs, sd) {
debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`);
this.dials.delete(sd.callSid);
@@ -426,12 +456,12 @@ class TaskDial extends Task {
this.callSid = sd.callSid;
if (this.earlyMedia) {
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
cs.propagateAnswer();
await cs.propagateAnswer();
}
if (this.timeLimit) {
this.timerMaxCallDuration = setTimeout(() => {
this.logger.info(`Dial:_selectSingleDial tearing down call as it has reached ${this.timeLimit}s`);
this.ep.unbridge();
this.ep && this.ep.unbridge();
this.kill(cs);
}, this.timeLimit * 1000);
}
@@ -441,8 +471,11 @@ class TaskDial extends Task {
if (this.dlg) {
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
sessionTracker.remove(this.callSid);
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
this.ep.unbridge();
if (this.timerMaxCallDuration) {
clearTimeout(this.timerMaxCallDuration);
this.timerMaxCallDuration = null;
}
this.ep && this.ep.unbridge();
this.kill(cs);
}
});
@@ -453,10 +486,14 @@ class TaskDial extends Task {
dialCallSid: sd.callSid,
});
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.ep, this.childDtmfCollector);
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep);
if (this.listenTask) this.listenTask.exec(cs, this.ep);
/* if we can release the media back to the SBC, do so now */
if (this.canReleaseMedia) this._releaseMedia(cs, sd);
}
_bridgeEarlyMedia(sd) {
@@ -468,6 +505,33 @@ class TaskDial extends Task {
}
}
/**
* Release the media from freeswitch
* @param {*} cs
* @param {*} sd
*/
async _releaseMedia(cs, sd) {
assert(cs.ep && sd.ep);
try {
this.logger.info('Dial:_releaseMedia - releasing media from freewitch');
const aLegSdp = cs.ep.remote.sdp;
const bLegSdp = sd.ep.remote.sdp;
await Promise.all[sd.releaseMediaToSBC(aLegSdp), cs.releaseMediaToSBC(bLegSdp)];
this.epOther = null;
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
} catch (err) {
this.logger.info({err}, 'Dial:_releaseMedia error');
}
}
async reAnchorMedia(cs, sd) {
if (cs.ep && sd.ep) return;
this.logger.info('Dial:reAnchorMedia - re-anchoring media to freewitch');
await Promise.all([sd.reAnchorMedia(), cs.reAnchorMedia()]);
this.epOther = cs.ep;
}
}
module.exports = TaskDial;

View File

@@ -210,6 +210,7 @@ class Dialogflow extends Task {
/* if we are using tts and a message was provided, play it out */
if (this.vendor && intent.fulfillmentText && intent.fulfillmentText.length > 0) {
const {srf} = cs;
const {stats} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers;
this.waitingForPlayStart = false;
@@ -229,7 +230,7 @@ class Dialogflow extends Task {
credentials: this.ttsCredentials
};
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
const {filePath, servedFromCache} = await synthAudio(obj);
const {filePath, servedFromCache} = await synthAudio(stats, obj);
if (filePath) cs.trackTmpFile(filePath);
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);

View File

@@ -3,7 +3,8 @@ const {
TaskName,
TaskPreconditions,
GoogleTranscriptionEvents,
AwsTranscriptionEvents
AwsTranscriptionEvents,
AzureTranscriptionEvents
} = require('../utils/constants');
const makeTask = require('./make_task');
@@ -33,6 +34,12 @@ class TaskGather extends Task {
this.vocabularyName = recognizer.vocabularyName;
this.vocabularyFilterName = recognizer.vocabularyFilterName;
this.filterMethod = recognizer.filterMethod;
/* microsoft options */
this.outputFormat = recognizer.outputFormat || 'simple';
this.profanityOption = recognizer.profanityOption || 'raw';
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
}
this.digitBuffer = '';
@@ -46,6 +53,8 @@ class TaskGather extends Task {
get name() { return TaskName.Gather; }
get needsStt() { return this.input.includes('speech'); }
get earlyMedia() {
return (this.sayTask && this.sayTask.earlyMedia) ||
(this.playTask && this.playTask.earlyMedia);
@@ -59,9 +68,9 @@ class TaskGather extends Task {
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
if (!this.sttCredentials) {
if (this.needsStt && !this.sttCredentials) {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but not creds supplied`);
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
@@ -94,7 +103,7 @@ class TaskGather extends Task {
}
if (this.input.includes('digits')) {
ep.on('dtmf', this._onDtmf.bind(this, ep));
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
}
await this.awaitTaskDone();
@@ -104,23 +113,25 @@ class TaskGather extends Task {
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
}
kill(cs) {
super.kill(cs);
this._killAudio();
this._killAudio(cs);
this.ep.removeAllListeners('dtmf');
this._resolve('killed');
}
_onDtmf(ep, evt) {
_onDtmf(cs, ep, evt) {
this.logger.debug(evt, 'TaskGather:_onDtmf');
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
else {
this.digitBuffer += evt.dtmf;
if (this.digitBuffer.length === this.numDigits) this._resolve('dtmf-num-digits');
}
this._killAudio();
this._killAudio(cs);
}
async _initSpeech(cs, ep) {
@@ -133,7 +144,9 @@ class TaskGather extends Task {
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
GOOGLE_SPEECH_MODEL: 'command_and_search'
});
if (this.hints && this.hints.length > 1) opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
if (this.hints && this.hints.length > 1) {
opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
if (this.altLanguages && this.altLanguages.length > 1) {
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
}
@@ -143,22 +156,41 @@ class TaskGather extends Task {
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
}
else {
else if (['aws', 'polly'].includes(this.vendor)) {
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
if (this.vocabularyFilterName) {
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
}
Object.assign(opts, {
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
if (this.sttCredentials) {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
}
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
}
else if ('microsoft' === this.vendor) {
if (this.sttCredentials) {
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
'AZURE_REGION': this.sttCredentials.region
});
}
if (this.hints && this.hints.length > 1) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
//if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
//if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoSpeechDetected.bind(this, cs, ep));
}
await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
}
_startTranscribing(ep) {
@@ -191,24 +223,36 @@ class TaskGather extends Task {
}
}
_killAudio() {
_killAudio(cs) {
if (this.sayTask && !this.sayTask.killed) {
this.sayTask.removeAllListeners('playDone');
this.sayTask.kill();
this.sayTask.kill(cs);
this.sayTask = null;
}
if (this.playTask && !this.playTask.killed) {
this.playTask.removeAllListeners('playDone');
this.playTask.kill();
this.playTask.kill(cs);
this.playTask = null;
}
}
_onTranscription(cs, ep, evt) {
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
this.logger.debug(evt, 'TaskGather:_onTranscription');
const final = evt.is_final;
if (final) {
this._resolve('speech', evt);
if ('microsoft' === this.vendor) {
const nbest = evt.NBest;
const newEvent = {
is_final: evt.RecognitionStatus === 'Success',
alternatives: [
{
confidence: nbest[0].Confidence,
transcript: nbest[0].Display
}
]
};
evt = newEvent;
}
this.logger.debug(evt, 'TaskGather:_onTranscription');
if (evt.is_final) this._resolve('speech', evt);
else if (this.partialResultHook) {
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
@@ -221,6 +265,10 @@ class TaskGather extends Task {
}
}
_onNoSpeechDetected(cs, ep) {
this._resolve('timeout');
}
async _resolve(reason, evt) {
if (this.resolved) return;
this.resolved = true;
@@ -233,14 +281,15 @@ class TaskGather extends Task {
this._clearTimer();
if (reason.startsWith('dtmf')) {
await this.performAction({digits: this.digitBuffer});
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
}
else if (reason.startsWith('speech')) {
if (this.parentTask) this.parentTask.emit('transcription', evt);
else await this.performAction({speech: evt});
else await this.performAction({speech: evt, reason: 'speechDetected'});
}
else if (reason.startsWith('timeout') && this.parentTask) {
this.parentTask.emit('timeout', evt);
else if (reason.startsWith('timeout')) {
if (this.parentTask) this.parentTask.emit('timeout', evt);
else await this.performAction({reason: 'timeout'});
}
this.notifyTaskDone();
}

View File

@@ -182,12 +182,13 @@ class Lex extends Task {
const type = messages[0].type;
if (['PlainText', 'SSML'].includes(type) && msg) {
const {srf} = cs;
const {stats} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers;
try {
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
// eslint-disable-next-line no-unused-vars
const {filePath, servedFromCache} = await synthAudio({
const {filePath, servedFromCache} = await synthAudio(stats, {
text: msg,
vendor: this.vendor,
language: this.language,

View File

@@ -17,8 +17,10 @@ function makeTask(logger, obj, parent) {
case TaskName.SipDecline:
const TaskSipDecline = require('./sip_decline');
return new TaskSipDecline(logger, data, parent);
case TaskName.SipRefer:
const TaskSipRefer = require('./sip_refer');
return new TaskSipRefer(logger, data, parent);
case TaskName.Conference:
logger.debug({data}, 'Conference verb');
const TaskConference = require('./conference');
return new TaskConference(logger, data, parent);
case TaskName.Dial:

View File

@@ -1,6 +1,7 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const bent = require('bent');
const { v4: uuidv4 } = require('uuid');
class TaskMessage extends Task {
constructor(logger, opts) {
@@ -8,13 +9,11 @@ class TaskMessage extends Task {
this.preconditions = TaskPreconditions.None;
this.payload = {
message_sid: this.data.message_sid,
provider: this.data.provider,
message_sid: this.data.message_sid || uuidv4(),
carrier: this.data.carrier,
to: this.data.to,
from: this.data.from,
cc: this.data.cc,
text: this.data.text,
media: this.data.media
text: this.data.text
};
}
@@ -28,16 +27,18 @@ class TaskMessage extends Task {
const {srf, accountSid} = cs;
const {res} = cs.callInfo;
let payload = this.payload;
const actionParams = {message_sid: this.payload.message_sid};
await super.exec(cs);
try {
const {getSBC, getSmpp, dbHelpers} = srf.locals;
const {getSmpp, dbHelpers} = srf.locals;
const {lookupSmppGateways} = dbHelpers;
this.logger.info(`looking up gateways for account_sid: ${accountSid}`);
this.logger.debug(`looking up gateways for account_sid: ${accountSid}`);
const r = await lookupSmppGateways(accountSid);
let gw, url, relativeUrl;
if (r.length > 0) {
gw = r.find((o) => 1 === o.sg.outbound && (!this.payload.provider || o.vc.name === this.payload.provider));
gw = r.find((o) => 1 === o.sg.outbound && (!this.payload.carrier || o.vc.name === this.payload.carrier));
}
if (gw) {
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
@@ -50,37 +51,75 @@ class TaskMessage extends Task {
};
}
else {
this.logger.info({gw, accountSid, provider: this.payload.provider},
//TMP: smpp only at the moment, need to add http back in
/*
this.logger.info({gw, accountSid, carrier: this.payload.carrier},
'Message:exec - no smpp gateways found to send message');
relativeUrl = 'v1/outboundSMS';
const sbcAddress = getSBC();
if (sbcAddress) url = `http://${sbcAddress}:3000/`;
//TMP: smpp only at the moment, need to add http back in
return res.sendStatus(404);
*/
this.performAction({
...actionParams,
message_status: 'no carriers'
}).catch((err) => {});
if (res) res.sendStatus(404);
return;
}
if (url) {
const post = bent(url, 'POST', 'json', 201);
const post = bent(url, 'POST', 'json', 201, 480);
this.logger.info({payload, url}, 'Message:exec sending outbound SMS');
const response = await post(relativeUrl, payload);
this.logger.info({response}, 'Successfully sent SMS');
if (cs.callInfo.res) {
this.logger.info('Message:exec sending 200 OK response to HTTP POST from api server');
res.status(200).json({
sid: cs.callInfo.messageSid,
providerResponse: response
});
const {smpp_err_code, carrier, message_id, message} = response;
if (smpp_err_code) {
this.logger.info({response}, 'SMPP error sending SMS');
this.performAction({
...actionParams,
carrier,
carrier_message_id: message_id,
message_status: 'failure',
message_failure_reason: message
}).catch((err) => {});
if (res) {
res.status(480).json({
...response,
sid: cs.callInfo.messageSid
});
}
}
else {
const {message_id, carrier} = response;
this.logger.info({response}, 'Successfully sent SMS');
this.performAction({
...actionParams,
carrier,
carrier_message_id: message_id,
message_status: 'success',
}).catch((err) => {});
if (res) {
res.status(200).json({
sid: cs.callInfo.messageSid,
carrierResponse: response
});
}
}
// TODO: action Hook
}
else {
this.logger.info('Message:exec - unable to send SMS as there are no available SMS gateways');
res.status(422).json({message: 'no configured SMS gateways'});
this.logger.info('Message:exec - unable to send SMS as SMPP is not configured on the system');
this.performAction({
...actionParams,
message_status: 'smpp configuration error'
}).catch((err) => {});
if (res) res.status(404).json({message: 'no configured SMS gateways'});
}
} catch (err) {
this.logger.error(err, 'TaskMessage:exec - Error sending SMS');
res.status(422).json({message: 'no configured SMS gateways'});
this.logger.error(err, 'TaskMessage:exec - unexpected error sending SMS');
this.performAction({
...actionParams,
message_status: 'system error',
message_failure_reason: err.message
});
if (res) res.status(422).json({message: 'no configured SMS gateways'});
}
}
}

View File

@@ -17,8 +17,12 @@ class TaskPlay extends Task {
await super.exec(cs);
this.ep = ep;
try {
while (!this.killed && this.loop--) {
await ep.play(this.url);
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
if (cs.isInConference) {
const {memberId, confName, confUuid} = cs;
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
}
else await ep.play(this.url);
}
} catch (err) {
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
@@ -30,7 +34,13 @@ class TaskPlay extends Task {
super.kill(cs);
if (this.ep.connected && !this.playComplete) {
this.logger.debug('TaskPlay:kill - killing audio');
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
if (cs.isInConference) {
const {memberId, confName} = cs;
this.killPlayToConfMember(this.ep, memberId, confName);
}
else {
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
}
}
}
}

View File

@@ -19,14 +19,17 @@ class TaskSay extends Task {
const {srf} = cs;
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
const {writeAlerts, AlertType} = srf.locals;
const {writeAlerts, AlertType, stats} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers;
const vendor = this.synthesizer.vendor || cs.speechSynthesisVendor;
const language = this.synthesizer.language || cs.speechSynthesisLanguage;
const voice = this.synthesizer.voice || cs.speechSynthesisVoice;
const hasVerbLevelTts = this.synthesizer.vendor && this.synthesizer.vendor !== 'default';
const vendor = hasVerbLevelTts ? this.synthesizer.vendor : cs.speechSynthesisVendor ;
const language = hasVerbLevelTts ? this.synthesizer.language : cs.speechSynthesisLanguage ;
const voice = hasVerbLevelTts ? this.synthesizer.voice : cs.speechSynthesisVoice ;
const salt = cs.callSid;
const credentials = cs.getSpeechCredentials(vendor, 'tts');
this.logger.info({language, voice}, `Task:say - using vendor: ${vendor}`);
this.ep = ep;
try {
if (!credentials) {
@@ -40,7 +43,7 @@ class TaskSay extends Task {
// synthesize all of the text elements
let lastUpdated = false;
const filepath = (await Promise.all(this.text.map(async(text) => {
const {filePath, servedFromCache} = await synthAudio({
const {filePath, servedFromCache} = await synthAudio(stats, {
text,
vendor,
language,
@@ -68,10 +71,14 @@ class TaskSay extends Task {
this.logger.debug({filepath}, 'synthesized files for tts');
while (!this.killed && this.loop-- && this.ep.connected) {
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
let segment = 0;
do {
await ep.play(filepath[segment]);
if (cs.isInConference) {
const {memberId, confName, confUuid} = cs;
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
}
else await ep.play(filepath[segment]);
} while (!this.killed && ++segment < filepath.length);
}
} catch (err) {
@@ -84,7 +91,13 @@ class TaskSay extends Task {
super.kill(cs);
if (this.ep.connected) {
this.logger.debug('TaskSay:kill - killing audio');
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
if (cs.isInConference) {
const {memberId, confName} = cs;
this.killPlayToConfMember(this.ep, memberId, confName);
}
else {
this.ep.api('uuid_break', this.ep.uuid);
}
}
}
}

101
lib/tasks/sip_refer.js Normal file
View File

@@ -0,0 +1,101 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const {parseUri} = require('drachtio-srf');
/**
* sends a sip REFER to transfer the existing call
*/
class TaskSipRefer extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.StableCall;
this.referTo = this.data.referTo;
this.referredBy = this.data.referredBy;
this.headers = this.data.headers || {};
this.eventHook = this.data.eventHook;
}
get name() { return TaskName.SipRefer; }
async exec(cs) {
super.exec(cs);
const {dlg} = cs;
const {referTo, referredBy} = this._normalizeReferHeaders(cs, dlg);
try {
this.notifyHandler = this._handleNotify.bind(this, cs, dlg);
dlg.on('notify', this.notifyHandler);
const response = await dlg.request({
method: 'REFER',
headers: {
...this.headers,
'Refer-To': referTo,
'Referred-By': referredBy
}
});
this.referStatus = response.status;
this.logger.info(`TaskSipRefer:exec - received ${this.referStatus} to REFER`);
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
if (this.referStatus === 202) {
await this.awaitTaskDone();
}
else await this.performAction({refer_status: this.referStatus});
} catch (err) {
this.logger.info({err}, 'TaskSipRefer:exec - error sending REFER');
}
}
async kill(cs) {
super.kill(cs);
const {dlg} = cs;
dlg.off('notify', this.notifyHandler);
}
async _handleNotify(cs, dlg, req, res) {
res.send(200);
const contentType = req.get('Content-Type');
this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`);
if (contentType === 'message/sipfrag') {
const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body);
if (arr) {
const status = arr[1];
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
if (this.eventHook) {
await cs.requestor.request(this.eventHook, {event: 'transfer-status', call_status: status});
}
if (status >= 200) {
await this.performAction({refer_status: 202, final_referred_call_status: status});
this.notifyTaskDone();
}
}
}
}
_normalizeReferHeaders(cs, dlg) {
let {referTo, referredBy} = this;
/* get IP address of the SBC to use as hostname if needed */
const {host} = parseUri(dlg.remote.uri);
if (!referTo.startsWith('<') && !referTo.startsWith('sip') && !referTo.startsWith('"')) {
/* they may have only provided a phone number/user */
referTo = `sip:${referTo}@${host}`;
}
if (!referredBy) {
/* default */
referredBy = cs.req?.callingNumber || dlg.local.uri;
this.logger.info({referredBy}, 'setting referredby');
}
if (!referredBy.startsWith('<') && !referredBy.startsWith('sip') && !referredBy.startsWith('"')) {
/* they may have only provided a phone number/user */
referredBy = `sip:${referredBy}@${host}`;
}
return {referTo, referredBy};
}
}
module.exports = TaskSipRefer;

View File

@@ -9,6 +9,18 @@
"status"
]
},
"sip:refer": {
"properties": {
"referTo": "string",
"referredBy": "string",
"headers": "object",
"actionHook": "object|string",
"eventHook": "object|string"
},
"required": [
"referTo"
]
},
"dequeue": {
"properties": {
"name": "string",
@@ -46,7 +58,7 @@
"play": {
"properties": {
"url": "string",
"loop": "number",
"loop": "number|string",
"earlyMedia": "boolean"
},
"required": [
@@ -56,7 +68,7 @@
"say": {
"properties": {
"text": "string|array",
"loop": "number",
"loop": "number|string",
"synthesizer": "#synthesizer",
"earlyMedia": "boolean"
},
@@ -87,11 +99,13 @@
"startConferenceOnEnter": "boolean",
"endConferenceOnExit": "boolean",
"maxParticipants": "number",
"joinMuted": "boolean",
"actionHook": "object|string",
"waitHook": "object|string",
"statusEvents": "array",
"statusHook": "object|string",
"enterHook": "object|string"
"enterHook": "object|string",
"record": "#record"
},
"required": [
"name"
@@ -240,6 +254,14 @@
"url"
]
},
"record": {
"properties": {
"path": "string"
},
"required": [
"path"
]
},
"redirect": {
"properties": {
"actionHook": "object|string"
@@ -326,7 +348,7 @@
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "polly", "default"]
"enum": ["google", "aws", "polly", "microsoft", "default"]
},
"language": "string",
"voice": "string",
@@ -343,7 +365,7 @@
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "default"]
"enum": ["google", "aws", "microsoft", "default"]
},
"language": "string",
"hints": "array",
@@ -383,7 +405,24 @@
"mask",
"tag"
]
}
},
"outputFormat": {
"type": "string",
"enum": [
"simple",
"detailed"
]
},
"profanityOption": {
"type": "string",
"enum": [
"masked",
"removed",
"raw"
]
},
"requestSnr": "boolean",
"initialSpeechTimeoutMs": "number"
},
"required": [
"vendor"

View File

@@ -23,6 +23,9 @@ class Task extends Emitter {
this._killInProgress = false;
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
/* used when we play a prompt to a member in conference */
this._confPlayCompletionPromise = new Promise((resolve) => this._confPlayCompletionResolver = resolve);
}
/**
@@ -77,6 +80,21 @@ class Task extends Emitter {
return this._completionPromise;
}
/**
* when a play to conference member completes
*/
notifyConfPlayDone() {
this._confPlayCompletionResolver();
}
/**
* when a subclass task has launched various async activities and is now simply waiting
* for them to complete it should call this method to block until that happens
*/
awaitConfPlayDone() {
return this._confPlayCompletionPromise;
}
/**
* provided as a convenience for tasks, this simply calls CallSession#normalizeUrl
*/
@@ -118,6 +136,43 @@ class Task extends Emitter {
cs.replaceApplication(tasks);
}
async playToConfMember(ep, memberId, confName, confUuid, filepath) {
try {
this.logger.debug(`Task:playToConfMember - playing ${filepath} to ${confName}:${memberId}`);
// listen for conference events
const handler = this.__onConferenceEvent.bind(this);
ep.conn.on('esl::event::CUSTOM::*', handler) ;
const response = await ep.api(`conference ${confName} play ${filepath} ${memberId}`);
this.logger.debug({response}, 'Task:playToConfMember - api call returned');
await this.awaitConfPlayDone();
ep.conn.removeListener('esl::event::CUSTOM::*', handler);
} catch (err) {
this.logger.error({err}, `Task:playToConfMember - error playing ${filepath} to ${confName}:${memberId}`);
}
}
async killPlayToConfMember(ep, memberId, confName) {
try {
this.logger.debug(`Task:killPlayToConfMember - killing audio to ${confName}:${memberId}`);
const response = await ep.api(`conference ${confName} stop ${memberId}`);
this.logger.debug({response}, 'Task:killPlayToConfMember - api call returned');
} catch (err) {
this.logger.error({err}, `Task:killPlayToConfMember - error killing audio to ${confName}:${memberId}`);
}
}
__onConferenceEvent(evt) {
const eventName = evt.getHeader('Event-Subclass') ;
if (eventName === 'conference::maintenance') {
const action = evt.getHeader('Action') ;
if (action === 'play-file-member-done') {
this.logger.debug('done playing file to conf member');
this.notifyConfPlayDone();
}
}
}
async transferCallToFeatureServer(cs, sipAddress, opts) {
const uuid = uuidv4();
const {addKey} = cs.srf.locals.dbHelpers;

View File

@@ -3,6 +3,7 @@ const {
TaskName,
TaskPreconditions,
GoogleTranscriptionEvents,
AzureTranscriptionEvents,
AwsTranscriptionEvents
} = require('../utils/constants');
@@ -38,6 +39,12 @@ class TaskTranscribe extends Task {
this.vocabularyName = recognizer.vocabularyName;
this.vocabularyFilterName = recognizer.vocabularyFilterName;
this.filterMethod = recognizer.filterMethod;
/* microsoft options */
this.outputFormat = recognizer.outputFormat || 'simple';
this.profanityOption = recognizer.profanityOption || 'raw';
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
}
get name() { return TaskName.Transcribe; }
@@ -53,7 +60,13 @@ class TaskTranscribe extends Task {
try {
if (!this.sttCredentials) {
// TODO: generate alert (actually should be done by cs.getSpeechCredentials)
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
throw new Error('no provisioned speech credentials for TTS');
}
await this._startTranscribing(cs, ep);
@@ -70,6 +83,8 @@ class TaskTranscribe extends Task {
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
}
async kill(cs) {
@@ -91,11 +106,13 @@ class TaskTranscribe extends Task {
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, ep));
this._onMaxDurationExceeded.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep));
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep));
if (this.vendor === 'google') {
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
@@ -164,6 +181,22 @@ class TaskTranscribe extends Task {
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
}
else if (this.vendor === 'microsoft') {
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
'AZURE_REGION': this.sttCredentials.region
});
if (this.hints && this.hints.length > 1) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
if (this.outputFormat !== 'simple') opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with azure'));
}
await this._transcribe(ep);
}
@@ -178,6 +211,20 @@ class TaskTranscribe extends Task {
_onTranscription(cs, ep, evt) {
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
if ('microsoft' === this.vendor) {
const nbest = evt.NBest;
const alternatives = nbest.map((n) => {
return {
confidence: n.Confidence,
transcript: n.Display
};
});
const newEvent = {
is_final: evt.RecognitionStatus === 'Success',
alternatives
};
evt = newEvent;
}
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))

View File

@@ -18,6 +18,7 @@
"Redirect": "redirect",
"RestDial": "rest:dial",
"SipDecline": "sip:decline",
"SipRefer": "sip:refer",
"SipNotify": "sip:notify",
"SipRedirect": "sip:redirect",
"Say": "say",
@@ -64,6 +65,12 @@
"NoAudioDetected": "aws_transcribe::no_audio_detected",
"MaxDurationExceeded": "aws_transcribe::max_duration_exceeded"
},
"AzureTranscriptionEvents": {
"Transcription": "azure_transcribe::transcription",
"StartOfUtterance": "azure_transcribe::start_of_utterance",
"EndOfUtterance": "azure_transcribe::end_of_utterance",
"NoSpeechDetected": "azure_transcribe::no_speech_detected"
},
"ListenEvents": {
"Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed",

View File

@@ -21,6 +21,11 @@ const speechMapper = (cred) => {
obj.access_key_id = o.access_key_id;
obj.secret_access_key = o.secret_access_key;
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.region = o.region;
}
return obj;
};
@@ -37,7 +42,8 @@ module.exports = (logger, srf) => {
/* search at the service provider level if we don't find it at the account level */
const haveGoogle = speech.find((s) => s.vendor === 'google');
const haveAws = speech.find((s) => s.vendor === 'aws');
if (!haveGoogle || !haveAws) {
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
if (!haveGoogle || !haveAws || !haveMicrosoft) {
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
if (r3.length) {
if (!haveGoogle) {
@@ -48,6 +54,10 @@ module.exports = (logger, srf) => {
const aws = r3.find((s) => s.vendor === 'aws');
if (aws) speech.push(speechMapper(aws));
}
if (!haveMicrosoft) {
const ms = r3.find((s) => s.vendor === 'microsoft');
if (ms) speech.push(speechMapper(ms));
}
}
}

View File

@@ -1,5 +1,5 @@
const crypto = require('crypto');
const algorithm = 'aes-256-ctr';
const algorithm = 'aes-256-cbc';
const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256')
.update(String(process.env.JWT_SECRET))

View File

@@ -135,6 +135,7 @@ function installSrfLocals(srf, logger) {
retrieveSet,
addToSet,
removeFromSet,
monitorSet,
pushBack,
popFront,
removeFromList,
@@ -178,6 +179,7 @@ function installSrfLocals(srf, logger) {
retrieveSet,
addToSet,
removeFromSet,
monitorSet,
pushBack,
popFront,
removeFromList,

View File

@@ -17,7 +17,7 @@ const moment = require('moment');
const { v4: uuidv4 } = require('uuid');
class SingleDialer extends Emitter {
constructor({logger, sbcAddress, target, opts, application, callInfo}) {
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo}) {
super();
assert(target.type);
@@ -31,6 +31,8 @@ class SingleDialer extends Emitter {
this.bindings = logger.bindings();
this.parentCallInfo = callInfo;
this.accountInfo = accountInfo;
this.callGone = false;
this.callSid = uuidv4();
@@ -62,6 +64,7 @@ class SingleDialer extends Emitter {
opts = opts || {};
opts.headers = opts.headers || {};
opts.headers = {...opts.headers, 'X-Call-Sid': this.callSid};
this.ms = ms;
let uri, to;
try {
switch (this.target.type) {
@@ -85,6 +88,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) {
@@ -195,7 +204,7 @@ class SingleDialer extends Emitter {
const duration = moment().diff(connectTime, 'seconds');
this.logger.debug('SingleDialer:exec called party hung up');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.ep.destroy();
this.ep && this.ep.destroy();
})
.on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas'))
.on('modify', async(req, res) => {
@@ -293,20 +302,48 @@ class SingleDialer extends Emitter {
this.logger = logger;
this.adulting = true;
this.emit('adulting');
await this.ep.unbridge()
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
this.ep.play('silence_stream://1000');
if (this.ep) {
await this.ep.unbridge()
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
this.ep.play('silence_stream://1000');
}
else {
await this.reAnchorMedia();
}
const cs = new AdultingCallSession({
logger: this.logger,
singleDialer: this,
application,
callInfo: this.callInfo,
accountInfo: this.accountInfo,
tasks
});
cs.exec();
return cs;
}
async releaseMediaToSBC(remoteSdp) {
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
await this.dlg.modify(remoteSdp, {
headers: {
'X-Reason': 'release-media'
}
});
this.ep.destroy()
.then(() => this.ep = null)
.catch((err) => this.logger.error({err}, 'SingleDialer:releaseMediaToSBC: Error destroying endpoint'));
}
async reAnchorMedia() {
assert(this.dlg && this.dlg.connected && !this.ep);
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
await this.dlg.modify(this.ep.local.sdp, {
headers: {
'X-Reason': 'anchor-media'
}
});
}
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
(!duration && callStatus !== CallStatus.Completed),
@@ -329,9 +366,9 @@ class SingleDialer extends Emitter {
}
}
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo}) {
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo}) {
const myOpts = deepcopy(opts);
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo});
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo, accountInfo});
sd.exec(srf, ms, myOpts);
return sd;
}

View File

@@ -100,15 +100,15 @@ class Requestor {
assert.ok(url, 'Requestor:request url was not provided');
assert.ok, (['GET', 'POST'].includes(method), `Requestor:request method must be 'GET' or 'POST' not ${method}`);
this.logger.debug({hook, payload}, `Requestor:request ${method} ${url}`);
const {url: urlInfo = hook, method: methodInfo = 'POST'} = hook; // mask user/pass
this.logger.debug({url: urlInfo, method: methodInfo, payload}, `Requestor:request ${method} ${url}`);
const startAt = process.hrtime();
let buf;
try {
const sigHeader = generateSigHeader(payload, this.secret);
const headers = {...sigHeader, ...this.authHeader};
this.logger.info({url, headers}, 'send webhook');
//this.logger.info({url, headers}, 'send webhook');
buf = isRelativeUrl(url) ?
await this.post(url, payload, headers) :
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);

View File

@@ -7,13 +7,15 @@ const debug = require('debug')('jambonz:feature-server');
module.exports = (logger) => {
logger = logger || noopLogger;
let idxSbc = 0;
let sbcs = [];
assert.ok(process.env.JAMBONES_SBCS, 'missing JAMBONES_SBCS env var');
const sbcs = process.env.JAMBONES_SBCS
.split(',')
.map((sbc) => sbc.trim());
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
logger.info({sbcs}, 'SBC inventory');
if (process.env.JAMBONES_SBCS) {
sbcs = process.env.JAMBONES_SBCS
.split(',')
.map((sbc) => sbc.trim());
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
logger.info({sbcs}, 'SBC inventory');
}
// listen for SNS lifecycle changes
let lifecycleEmitter = new Emitter();
@@ -96,8 +98,19 @@ module.exports = (logger) => {
}, 20000);
// initial ping once we are up
setTimeout(() => {
setTimeout(async() => {
const {srf} = require('../..');
// if SBCs are auto-scaling, monitor them as they come and go
if (!process.env.JAMBONES_SBCS) {
const {monitorSet} = srf.locals.dbHelpers;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-sip`;
await monitorSet(setName, 10, (members) => {
sbcs = members;
logger.info(`sbc-pinger: SBC roster has changed, list of active SBCs is now ${sbcs}`);
});
}
pingProxies(srf);
}, 1000);

1794
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-feature-server",
"version": "0.6.6",
"version": "0.6.7-rc5",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -26,9 +26,9 @@
"jslint": "eslint app.js lib"
},
"dependencies": {
"@jambonz/db-helpers": "^0.6.13",
"@jambonz/db-helpers": "^0.6.14",
"@jambonz/mw-registrar": "^0.2.1",
"@jambonz/realtimedb-helpers": "^0.4.1",
"@jambonz/realtimedb-helpers": "^0.4.9",
"@jambonz/stats-collector": "^0.1.5",
"@jambonz/time-series": "^0.1.5",
"aws-sdk": "^2.846.0",
@@ -36,8 +36,8 @@
"cidr-matcher": "^2.1.1",
"debug": "^4.3.1",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^2.0.7",
"drachtio-srf": "^4.4.50",
"drachtio-fsmrf": "^2.0.11",
"drachtio-srf": "^4.4.55",
"express": "^4.17.1",
"ip": "^1.1.5",
"moment": "^2.29.1",

View File

@@ -587,7 +587,8 @@ DROP TABLE IF EXISTS `speech_credentials`;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `speech_credentials` (
`speech_credential_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`service_provider_sid` CHAR(36),
`account_sid` char(36) NOT NULL,
`vendor` varchar(255) NOT NULL,
`credential` VARCHAR(8192) NOT NULL,
`use_for_tts` tinyint(1) DEFAULT '1',
@@ -611,7 +612,7 @@ CREATE TABLE `speech_credentials` (
LOCK TABLES `speech_credentials` WRITE;
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
UNLOCK TABLES;

View File

@@ -20,12 +20,16 @@ DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways;
DROP TABLE IF EXISTS predefined_carriers;
DROP TABLE IF EXISTS account_offers;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS schema_version;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS sbc_addresses;
@@ -148,6 +152,20 @@ predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_sip_gateway_sid)
);
CREATE TABLE predefined_smpp_gateways
(
predefined_smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. ',
port INTEGER NOT NULL DEFAULT 2775 COMMENT 'smpp signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound SMS from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'i',
netmask INTEGER NOT NULL DEFAULT 32,
is_primary BOOLEAN NOT NULL DEFAULT 1,
use_tls BOOLEAN DEFAULT 0,
predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_smpp_gateway_sid)
);
CREATE TABLE products
(
product_sid CHAR(36) NOT NULL UNIQUE ,
@@ -174,6 +192,11 @@ stripe_product_id VARCHAR(56) NOT NULL,
PRIMARY KEY (account_offer_sid)
);
CREATE TABLE schema_version
(
version VARCHAR(16)
);
CREATE TABLE api_keys
(
api_key_sid CHAR(36) NOT NULL UNIQUE ,
@@ -420,6 +443,10 @@ CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefin
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_smpp_gateway_sid_idx ON predefined_smpp_gateways (predefined_smpp_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_smpp_gateways (predefined_carrier_sid);
ALTER TABLE predefined_smpp_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk_1 (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX product_sid_idx ON products (product_sid);
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
@@ -545,4 +572,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;
SET FOREIGN_KEY_CHECKS=0;