Compare commits

..

27 Commits

Author SHA1 Message Date
snyk-bot
7842a3ebae fix: upgrade pino from 6.13.3 to 6.13.4
Snyk has created this PR to upgrade pino from 6.13.3 to 6.13.4.

See this package in npm:
https://www.npmjs.com/package/pino

See this project in Snyk:
https://app.snyk.io/org/davehorton/project/cec90d0e-0ded-433e-a42e-fe78b28ae489?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-02-03 06:35:30 +00:00
Dave Horton
2329f0cda0 child tasks must remove reference to parent on kill or else entangled parent-child tasks will not be gc'ed 2022-02-01 11:00:12 -05:00
Dave Horton
36683dc151 bugfix: include custom jambonz headers on rest outdial 2022-01-28 13:36:06 -05:00
Dave Horton
ce738a7852 0.7.2 version 2022-01-28 09:16:05 -05:00
Dave Horton
77a696a0dc update to latest synthAudio with minor fixes 2022-01-27 13:52:35 -05:00
Dave Horton
62ff44540d more changes for wellsaid 2022-01-27 10:55:32 -05:00
Dave Horton
e5821cddf8 further fix for wellsaid tts 2022-01-27 10:46:16 -05:00
Dave Horton
25567a7842 add support for retrieving wellsaid speech credential 2022-01-27 10:34:30 -05:00
Dave Horton
40bd3c9c88 update to realtimedb-helpers with support for wellsaid tts 2022-01-27 10:13:18 -05:00
Dave Horton
27d6d32359 bugfix: rtpengine needs to transcode when different codecs are used on A and B legs 2022-01-26 07:37:09 -05:00
Dave Horton
142f5d409f use smpp service name when running in kubernetes 2022-01-25 13:29:16 -05:00
Dave Horton
da4a7184a4 update to realtimedb-helpers with engine caching fix for tts 2022-01-22 15:35:01 -05:00
Dave Horton
2c72bf50cd sync package-lock.json 2022-01-21 22:04:07 -05:00
Dave Horton
b27f349fc6 linting 2022-01-21 10:15:33 -05:00
Dave Horton
138aa5836a lock version 2022-01-21 10:13:42 -05:00
Dave Horton
e1a023c21e bugfix: aws property is engine not platform 2022-01-21 09:57:58 -05:00
Dave Horton
8acb4d1a24 #58 - add support for platform (standard, or neural) when using aws tts 2022-01-19 19:46:24 -05:00
Dave Horton
26d4bfb63b Cognigy: settings tweaks 2022-01-18 19:49:46 -05:00
Dave Horton
45dcab8517 fix linting error 2022-01-17 20:37:32 -05:00
Dave Horton
27e3cba00b fix vulnerabilities 2022-01-17 18:41:12 -05:00
Dave Horton
097f36cb00 bugix: re-invites after releasing media fail 2022-01-17 13:11:19 -05:00
Dave Horton
752eed428f cognigy: when use azuyre tts, request detailed output format 2022-01-14 08:48:55 -05:00
Dave Horton
afb874aabc minor logging change 2022-01-14 07:56:11 -05:00
Dave Horton
59227febf9 K8s (#57)
* JAMBONES_NETWORK_CIDR not needed for K8S

* fix bug setting fsUUID in K8S scenario

* bugfix: dial music was not stopped when a dial verb times out (#56)
2022-01-09 14:57:46 -05:00
Dave Horton
8593f12b51 add custom headers to outdial, save unique uuid for running FS to redis 2022-01-08 11:50:18 -05:00
Dave Horton
3bf1984854 K8s changes (#55)
* K8S: dont send OPTIONS pings

* fix missing ref

* k8s pre-stop hook added

* k8s pre-stop hook changes

* chmod +x utility

* more k8s pre-stop changes

* pre stop

* fix healthcheck

* k8s pre-stop working

* add readiness probe

* fix bug in pre-stop

* logging

* revamp k8s pre-stop a bit

* initial support for cognigy bot

* more cognigy changes

* switch to use transcribe for cognigy

* #54 include callInfo in dialogflow event payload
2022-01-06 12:41:14 -05:00
Dave Horton
0e45e9b27c add target.overrideTo to specs 2021-12-22 08:32:56 -05:00
25 changed files with 1346 additions and 460 deletions

View File

@@ -1,4 +1,4 @@
FROM node:16
FROM node:17.4-slim
WORKDIR /opt/app/
COPY package.json ./
RUN npm install

9
app.js
View File

@@ -7,7 +7,7 @@ assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACH
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR, 'missing JAMBONES_SUBNET env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
const Srf = require('drachtio-srf');
const srf = new Srf();
@@ -17,7 +17,7 @@ const opts = {
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const logger = require('pino')(opts);
const {LifeCycleEvents} = require('./lib/utils/constants');
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
const installSrfLocals = require('./lib/utils/install-srf-locals');
installSrfLocals(srf, logger);
@@ -92,6 +92,10 @@ sessionTracker.on('idle', () => {
}
});
const getCount = () => sessionTracker.count;
const healthCheck = require('@jambonz/http-health-check');
healthCheck({app, logger, path: '/', fn: getCount});
setInterval(() => {
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
}, 5000);
@@ -113,6 +117,7 @@ function handle(signal) {
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
removeFromSet(setName, srf.locals.localSipAddress);
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
srf.locals.disabled = true;
}

29
bin/k8s-pre-stop-hook.js Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env node
const bent = require('bent');
const getJSON = bent('json');
const PORT = process.env.HTTP_PORT || 3000;
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
(async function() {
try {
do {
const obj = await getJSON(`http://127.0.0.1:${PORT}/`);
const {calls} = obj;
if (calls === 0) {
console.log('no calls on the system, we can exit');
process.exit(0);
}
else {
console.log(`waiting for ${calls} to exit..`);
}
await sleep(10000);
} while (1);
} catch (err) {
console.error(err, 'Error querying health endpoint');
process.exit(-1);
}
})();

View File

@@ -35,6 +35,8 @@ router.post('/', async(req, res) => {
opts.headers = {
...opts.headers,
'X-Jambonz-Routing': target.type,
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
'X-Call-Sid': callSid,
'X-Account-Sid': req.body.account_sid
};

View File

@@ -9,8 +9,4 @@ api.use('/enqueue', require('./enqueue'));
api.use('/messaging', require('./messaging')); // inbound SMS
api.use('/createMessage', require('./create-message')); // outbound SMS (REST)
// health checks
api.get('/', (req, res) => res.sendStatus(200));
api.get('/health', (req, res) => res.sendStatus(200));
module.exports = api;

View File

@@ -1,16 +1,23 @@
const express = require('express');
const api = require('./api');
const routes = express.Router();
const sessionTracker = require('../session/session-tracker');
const readiness = (req, res) => {
const logger = req.app.locals.logger;
const {count} = sessionTracker;
const {srf} = require('../..');
const {getFreeswitch} = srf.locals;
if (getFreeswitch()) {
return res.status(200).json({calls: count});
}
logger.info('responding to /health check with failure as freeswitch is not up');
res.sendStatus(480);
};
routes.use('/v1', api);
// health checks
routes.get('/', (req, res) => {
res.sendStatus(200);
});
routes.get('/health', (req, res) => {
res.sendStatus(200);
});
// health check
routes.get('/health', readiness);
module.exports = routes;

View File

@@ -264,6 +264,12 @@ class CallSession extends Emitter {
region: credential.region
};
}
else if ('wellsaid' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key
};
}
}
else {
writeAlerts({
@@ -780,9 +786,19 @@ class CallSession extends Emitter {
async _onReinvite(req, res) {
try {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
if (this.ep) {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
}
else if (this.currentTask && this.currentTask.name === TaskName.Dial) {
this.logger.info('handling reINVITE after media has been released');
await this.currentTask.handleReinviteAfterMediaReleased(req, res);
}
else {
this.logger.info('got reINVITE but no endpoint and media has not been released');
res.send(488);
}
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
}
@@ -953,6 +969,13 @@ class CallSession extends Emitter {
});
}
async handleReinviteAfterMediaReleased(req, res) {
assert(this.dlg && this.dlg.connected && !this.ep);
const sdp = await this.dlg.modify(req.body);
this.logger.info({sdp}, 'CallSession:handleReinviteAfterMediaReleased - reinvite to A leg returned sdp');
res.send(200, {body: sdp});
}
/**
* Called any time call status changes. This method both invokes the
* call_status_hook callback as well as updates the realtime database

248
lib/tasks/cognigy.js Normal file
View File

@@ -0,0 +1,248 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const makeTask = require('./make_task');
const { SocketClient } = require('@cognigy/socket-client');
const parseGallery = (obj = {}) => {
const {_default} = obj;
if (_default) {
const {_gallery} = _default;
if (_gallery) return _gallery.fallbackText;
}
};
const parseQuickReplies = (obj) => {
const {_default} = obj;
if (_default) {
const {_quickReplies} = _default;
if (_quickReplies) return _quickReplies.text || _quickReplies.fallbackText;
}
};
const parseBotText = (evt) => {
const {text, data} = evt;
if (text) return text;
switch (data?.type) {
case 'quickReplies':
return parseQuickReplies(data?._cognigy);
case 'gallery':
return parseGallery(data?._cognigy);
default:
break;
}
};
class Cognigy extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
this.url = this.data.url;
this.token = this.data.token;
this.prompt = this.data.prompt;
this.eventHook = this.data?.eventHook;
this.actionHook = this.data?.actionHook;
this.data = this.data.data || {};
this.prompts = [];
}
get name() { return TaskName.Cognigy; }
get hasReportedFinalAction() {
return this.reportedFinalAction || this.isReplacingApplication;
}
async exec(cs, ep) {
await super.exec(cs);
this.ep = ep;
try {
/* set event handlers and start transcribing */
this.on('transcription', this._onTranscription.bind(this, cs, ep));
this.on('error', this._onError.bind(this, cs, ep));
this.transcribeTask = this._makeTranscribeTask();
this.transcribeTask.exec(cs, ep, this)
.catch((err) => {
this.logger.info({err}, 'Cognigy transcribe task returned error');
this.notifyTaskDone();
});
if (this.prompt) {
this.sayTask = this._makeSayTask(this.prompt);
this.sayTask.exec(cs, ep, this)
.catch((err) => {
this.logger.info({err}, 'Cognigy say task returned error');
this.notifyTaskDone();
});
}
/* connect to the bot and send initial data */
this.client = new SocketClient(
this.url,
this.token,
{
sessionId: cs.callSid,
channel: 'jambonz',
forceWebsockets: true,
reconnection: true,
settings: {
enableTypingIndicator: false
}
}
);
this.client.on('output', this._onBotUtterance.bind(this, cs, ep));
this.client.on('typingStatus', this._onBotTypingStatus.bind(this, cs, ep));
this.client.on('error', this._onBotError.bind(this, cs, ep));
this.client.on('finalPing', this._onBotFinalPing.bind(this, cs, ep));
await this.client.connect();
this.client.sendMessage('', {...this.data, ...cs.callInfo});
await this.awaitTaskDone();
} catch (err) {
this.logger.error({err}, 'Cognigy error');
throw err;
}
}
async kill(cs) {
super.kill(cs);
this.logger.debug('Cognigy:kill');
this.removeAllListeners();
this.transcribeTask && this.transcribeTask.kill();
this.client.removeAllListeners();
if (this.client && this.client.connected) this.client.disconnect();
if (!this.hasReportedFinalAction) {
this.reportedFinalAction = true;
this.performAction({cognigyResult: 'caller hungup'})
.catch((err) => this.logger.info({err}, 'cognigy - error w/ action webook'));
}
if (this.ep.connected) {
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
}
this.notifyTaskDone();
}
_makeTranscribeTask() {
const opts = {
recognizer: this.data.recognizer || {
vendor: 'default',
language: 'default',
outputFormat: 'detailed'
}
};
this.logger.debug({opts}, 'constructing a nested transcribe object');
const transcribe = makeTask(this.logger, {transcribe: opts}, this);
return transcribe;
}
_makeSayTask(text) {
const opts = {
text,
synthesizer: this.data.synthesizer ||
{
vendor: 'default',
language: 'default',
voice: 'default'
}
};
this.logger.debug({opts}, 'constructing a nested say object');
const say = makeTask(this.logger, {say: opts}, this);
return say;
}
async _onBotError(cs, ep, evt) {
this.logger.info({evt}, 'Cognigy:_onBotError');
this.performAction({cognigyResult: 'botError', message: evt.message });
this.reportedFinalAction = true;
this.notifyTaskDone();
}
async _onBotTypingStatus(cs, ep, evt) {
this.logger.info({evt}, 'Cognigy:_onBotTypingStatus');
}
async _onBotFinalPing(cs, ep) {
this.logger.info('Cognigy:_onBotFinalPing');
if (this.prompts.length) {
const text = this.prompts.join('.');
this.prompts = [];
if (text && !this.killed) {
this.sayTask = this._makeSayTask(text);
this.sayTask.exec(cs, ep, this)
.catch((err) => {
this.logger.info({err}, 'Cognigy say task returned error');
this.notifyTaskDone();
});
}
}
}
async _onBotUtterance(cs, ep, evt) {
this.logger.debug({evt}, 'Cognigy:_onBotUtterance');
if (this.eventHook) {
this.performHook(cs, this.eventHook, {event: 'botMessage', message: evt})
.then((redirected) => {
if (redirected) {
this.logger.info('Cognigy_onTranscription: event handler for bot message redirected us to new webhook');
this.reportedFinalAction = true;
this.performAction({cognigyResult: 'redirect'}, false);
}
return;
})
.catch(({err}) => {
this.logger.info({err}, 'Cognigy_onTranscription: error sending event hook');
});
}
const text = parseBotText(evt);
this.prompts.push(text);
}
async _onTranscription(cs, ep, evt) {
this.logger.debug({evt}, `Cognigy: got transcription for callSid ${cs.callSid}`);
const utterance = evt.alternatives[0].transcript;
if (this.eventHook) {
this.performHook(cs, this.eventHook, {event: 'userMessage', message: utterance})
.then((redirected) => {
if (redirected) {
this.logger.info('Cognigy_onTranscription: event handler for user message redirected us to new webhook');
this.reportedFinalAction = true;
this.performAction({cognigyResult: 'redirect'}, false);
if (this.transcribeTask) this.transcribeTask.kill(cs);
}
return;
})
.catch(({err}) => {
this.logger.info({err}, 'Cognigy_onTranscription: error sending event hook');
});
}
/* send the user utterance to the bot */
try {
if (this.client && this.client.connected) {
this.client.sendMessage(utterance);
}
else {
this.logger.info('Cognigy_onTranscription - not sending user utterance as bot is disconnected');
}
} catch (err) {
this.logger.error({err}, 'Cognigy_onTranscription: Error sending user utterance to Cognigy - ending task');
this.performAction({cognigyResult: 'socketError'});
this.reportedFinalAction = true;
this.notifyTaskDone();
}
}
_onError(cs, ep, err) {
this.logger.debug({err}, 'Cognigy: got error');
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
this.reportedFinalAction = true;
this.notifyTaskDone();
}
}
module.exports = Cognigy;

View File

@@ -161,6 +161,10 @@ class TaskDial extends Task {
async kill(cs, reason) {
super.kill(cs);
if (this.dialMusic && this.epOther) {
this.epOther.api('uuid_break', this.epOther.uuid)
.catch((err) => this.logger.info(err, 'Error killing dialMusic'));
}
this.killReason = reason || KillReason.Hangup;
if (this.timerMaxCallDuration) {
clearTimeout(this.timerMaxCallDuration);
@@ -433,6 +437,13 @@ class TaskDial extends Task {
this.kill(cs);
}
})
.on('reinvite', (req, res) => {
try {
cs.handleReinviteAfterMediaReleased(req, res);
} catch (err) {
this.logger.error(err, 'Error in dial einvite from B leg');
}
})
.once('adulting', () => {
/* child call just adulted and got its own session */
this.logger.info('Dial:on_adulting: detaching child call leg');
@@ -543,8 +554,7 @@ class TaskDial extends Task {
try {
const aLegSdp = cs.ep.remote.sdp;
const bLegSdp = sd.dlg.remote.sdp;
this.logger.debug({aLegSdp, bLegSdp}, 'Dial:_releaseMedia - releasing media from freewitch');
await Promise.all[sd.releaseMediaToSBC(aLegSdp), cs.releaseMediaToSBC(bLegSdp)];
await Promise.all[sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp), cs.releaseMediaToSBC(bLegSdp)];
this.epOther = null;
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
} catch (err) {
@@ -559,6 +569,12 @@ class TaskDial extends Task {
await Promise.all([sd.reAnchorMedia(), cs.reAnchorMedia()]);
this.epOther = cs.ep;
}
async handleReinviteAfterMediaReleased(req, res) {
const sdp = await this.dlg.modify(req.body);
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
res.send(200, {body: sdp});
}
}
module.exports = TaskDial;

View File

@@ -452,8 +452,8 @@ class Dialogflow extends Task {
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);
async _performHook(cs, hook, results = {}) {
const json = await this.cs.requestor.request(hook, {...results, ...cs.callInfo.toJSON()});
if (json && Array.isArray(json)) {
const makeTask = require('../make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));

View File

@@ -20,6 +20,9 @@ function makeTask(logger, obj, parent) {
case TaskName.SipRefer:
const TaskSipRefer = require('./sip_refer');
return new TaskSipRefer(logger, data, parent);
case TaskName.Cognigy:
const TaskCognigy = require('./cognigy');
return new TaskCognigy(logger, data, parent);
case TaskName.Conference:
const TaskConference = require('./conference');
return new TaskConference(logger, data, parent);

View File

@@ -42,7 +42,7 @@ class TaskMessage extends Task {
}
if (gw) {
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
url = getSmpp();
url = process.env.K8S ? 'http://smpp' : getSmpp();
relativeUrl = '/sms';
payload = {
...payload,

View File

@@ -25,7 +25,7 @@ class TaskSay extends Task {
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 engine = this.synthesizer.engine || 'standard';
const salt = cs.callSid;
const credentials = cs.getSpeechCredentials(vendor, 'tts');
@@ -48,6 +48,7 @@ class TaskSay extends Task {
vendor,
language,
voice,
engine,
salt,
credentials
}).catch((err) => {

View File

@@ -21,6 +21,22 @@
"referTo"
]
},
"cognigy": {
"properties": {
"url": "string",
"token": "string",
"recognizer": "#recognizer",
"tts": "#synthesizer",
"prompt": "string",
"actionHook": "object|string",
"eventHook": "object|string",
"data": "object"
},
"required": [
"url",
"token"
]
},
"dequeue": {
"properties": {
"name": "string",
@@ -308,7 +324,6 @@
"earlyMedia": "boolean"
},
"required": [
"transcriptionHook",
"recognizer"
]
},
@@ -329,7 +344,8 @@
"auth": "#auth",
"vmail": "boolean",
"tenant": "string",
"trunk": "string"
"trunk": "string",
"overrideTo": "string"
},
"required": [
"type"
@@ -353,6 +369,10 @@
},
"language": "string",
"voice": "string",
"engine": {
"type": "string",
"enum": ["standard", "neural"]
},
"gender": {
"type": "string",
"enum": ["MALE", "FEMALE", "NEUTRAL"]

View File

@@ -62,7 +62,9 @@ class Task extends Emitter {
kill(cs) {
if (this.cs && !this.cs.isConfirmCallSession) this.logger.debug(`${this.name} is being killed`);
this._killInProgress = true;
// no-op
/* remove reference to parent task or else entangled parent-child tasks will not be gc'ed */
setImmediate(() => this.parentTask = null);
}
/**

View File

@@ -11,6 +11,7 @@ class TaskTranscribe extends Task {
constructor(logger, opts, parentTask) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
this.parentTask = parentTask;
this.transcriptionHook = this.data.transcriptionHook;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
@@ -76,6 +77,7 @@ class TaskTranscribe extends Task {
await this.awaitTaskDone();
} catch (err) {
this.logger.info(err, 'TaskTranscribe:exec - error');
this.parentTask && this.parentTask.emit('error', err);
}
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected);
@@ -210,25 +212,36 @@ class TaskTranscribe extends Task {
}
_onTranscription(cs, ep, evt) {
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
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) => {
const alternatives = nbest ? nbest.map((n) => {
return {
confidence: n.Confidence,
transcript: n.Display
};
});
}) :
[
{
transcript: evt.DisplayText
}
];
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))
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
if (this.transcriptionHook) {
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
}
if (this.parentTask) {
this.parentTask.emit('transcription', evt);
}
if (this.killed) {
this.logger.debug('TaskTranscribe:_onTranscription exiting after receiving final transcription');
this._clearTimer();

View File

@@ -1,5 +1,6 @@
{
"TaskName": {
"Cognigy": "cognigy",
"Conference": "conference",
"Dequeue": "dequeue",
"Dial": "dial",
@@ -105,5 +106,6 @@
"Replaced": "replaced"
},
"MAX_SIMRINGS": 10,
"BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)"
"BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)",
"FS_UUID_SET_NAME": "fsUUIDs"
}

View File

@@ -36,6 +36,10 @@ const speechMapper = (cred) => {
obj.api_key = o.api_key;
obj.region = o.region;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
return obj;
};
@@ -53,6 +57,7 @@ module.exports = (logger, srf) => {
const haveGoogle = speech.find((s) => s.vendor === 'google');
const haveAws = speech.find((s) => s.vendor === 'aws');
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
if (!haveGoogle || !haveAws || !haveMicrosoft) {
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
if (r3.length) {
@@ -68,6 +73,10 @@ module.exports = (logger, srf) => {
const ms = r3.find((s) => s.vendor === 'microsoft');
if (ms) speech.push(speechMapper(ms));
}
if (!haveWellsaid) {
const wellsaid = r3.find((s) => s.vendor === 'wellsaid');
if (wellsaid) speech.push(speechMapper(wellsaid));
}
}
}

View File

@@ -5,15 +5,10 @@ const {TaskPreconditions, CallDirection} = require('../utils/constants');
const CallInfo = require('../session/call-info');
const assert = require('assert');
const ConfirmCallSession = require('../session/confirm-call-session');
const selectSbc = require('./select-sbc');
const Registrar = require('@jambonz/mw-registrar');
const AdultingCallSession = require('../session/adulting-call-session');
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 stripCodecs = require('./strip-ancillary-codecs');
const { v4: uuidv4 } = require('uuid');
class SingleDialer extends Emitter {
@@ -63,7 +58,12 @@ class SingleDialer extends Emitter {
async exec(srf, ms, opts) {
opts = opts || {};
opts.headers = opts.headers || {};
opts.headers = {...opts.headers, 'X-Call-Sid': this.callSid};
opts.headers = {
...opts.headers,
'X-Jambonz-Routing': this.target.type,
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
'X-Call-Sid': this.callSid
};
this.ms = ms;
let uri, to;
try {
@@ -84,7 +84,6 @@ class SingleDialer extends Emitter {
break;
case 'user':
assert(this.target.name);
const aor = this.target.name;
uri = `sip:${this.target.name}`;
to = this.target.name;
@@ -93,16 +92,6 @@ class SingleDialer extends Emitter {
'X-Override-To': this.target.overrideTo
});
}
// need to send to the SBC registered on
const reg = await registrar.query(aor);
if (reg) {
const sbc = selectSbc(reg.sbcAddress);
if (sbc) {
this.logger.debug(`SingleDialer:exec retrieved registration details for ${aor}, using sbc at ${sbc}`);
this.sbcAddress = sbc;
}
}
break;
case 'sip':
assert(this.target.sipUri);
@@ -209,9 +198,15 @@ class SingleDialer extends Emitter {
.on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas'))
.on('modify', async(req, res) => {
try {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
if (this.ep) {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
}
else {
this.logger.info('SingleDialer:exec: handling reINVITE with released media, emit event');
this.emit('reinvite', req, res);
}
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
}
@@ -322,9 +317,10 @@ class SingleDialer extends Emitter {
return cs;
}
async releaseMediaToSBC(remoteSdp) {
async releaseMediaToSBC(remoteSdp, localSdp) {
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
await this.dlg.modify(remoteSdp, {
const sdp = stripCodecs(this.logger, remoteSdp, localSdp) || remoteSdp;
await this.dlg.modify(sdp, {
headers: {
'X-Reason': 'release-media'
}

View File

@@ -1,8 +1,9 @@
const assert = require('assert');
const noopLogger = {info: () => {}, error: () => {}};
const {LifeCycleEvents} = require('./constants');
const { v4: uuidv4 } = require('uuid');
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants');
const Emitter = require('events');
const debug = require('debug')('jambonz:feature-server');
const noopLogger = {info: () => {}, error: () => {}};
module.exports = (logger) => {
logger = logger || noopLogger;
@@ -67,7 +68,6 @@ module.exports = (logger) => {
})();
}
// send OPTIONS pings to SBCs
async function pingProxies(srf) {
if (process.env.NODE_ENV === 'test') return;
@@ -90,29 +90,40 @@ module.exports = (logger) => {
}
}
}
if (process.env.K8S) {
setImmediate(() => {
logger.info('disabling OPTIONS pings since we are running as a kubernetes service');
const {srf} = require('../..');
const {addToSet} = srf.locals.dbHelpers;
const uuid = srf.locals.fsUUID = uuidv4();
addToSet(FS_UUID_SET_NAME, uuid)
.catch((err) => logger.info({err}, `Error adding ${uuid} to set ${FS_UUID_SET_NAME}`));
});
}
else {
// OPTIONS ping the SBCs from each feature server every 60 seconds
setInterval(() => {
const {srf} = require('../..');
pingProxies(srf);
}, process.env.OPTIONS_PING_INTERVAL || 30000);
// OPTIONS ping the SBCs from each feature server every 60 seconds
setInterval(() => {
const {srf} = require('../..');
pingProxies(srf);
}, process.env.OPTIONS_PING_INTERVAL || 30000);
// initial ping once we are up
setTimeout(async() => {
// initial ping once we are up
setTimeout(async() => {
const {srf} = require('../..');
// if SBCs are auto-scaling, monitor them as they come and go
const {srf} = require('../..');
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}`);
});
}
// 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);
pingProxies(srf);
}, 1000);
}
return {
lifecycleEmitter,

View File

@@ -1,13 +0,0 @@
const CIDRMatcher = require('cidr-matcher');
const matcher = new CIDRMatcher([process.env.JAMBONES_NETWORK_CIDR]);
module.exports = (sbcList) => {
const obj = sbcList
.split(',')
.map((str) => {
const arr = /^(.*)\/(.*):(\d+)$/.exec(str);
return {protocol: arr[1], host: arr[2], port: arr[3]};
})
.find((obj) => 'udp' == obj.protocol && matcher.contains(obj.host));
if (obj) return `${obj.host}:${obj.port}`;
};

View File

@@ -0,0 +1,30 @@
const sdpTransform = require('sdp-transform');
const stripCodecs = (logger, remoteSdp, localSdp) => {
try {
const sdp = sdpTransform.parse(remoteSdp);
const local = sdpTransform.parse(localSdp);
const m = local.media
.find((m) => 'audio' === m.type);
const pt = m.rtp[0].payload;
/* manipulate on the audio section */
const audio = sdp.media.find((m) => 'audio' === m.type);
/* discard all of the codecs except the first in our 200 OK, and telephony-events */
const ptSaves = audio.rtp
.filter((r) => r.codec === 'telephone-event' || r.payload === pt)
.map((r) => r.payload);
const rtp = audio.rtp.filter((r) => ptSaves.includes(r.payload));
/* reattach the new rtp sections and stripped payload list */
audio.rtp = rtp;
audio.payloads = rtp.map((r) => r.payload).join(' ');
return sdpTransform.write(sdp);
} catch (err) {
logger.error({err, remoteSdp, localSdp}, 'strip-ancillary-codecs error');
}
};
module.exports = stripCodecs;

1203
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": "v0.7.1",
"version": "v0.7.2",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -26,23 +26,25 @@
"jslint": "eslint app.js lib"
},
"dependencies": {
"@cognigy/socket-client": "^4.5.5",
"@jambonz/db-helpers": "^0.6.16",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "^0.2.1",
"@jambonz/realtimedb-helpers": "^0.4.14",
"@jambonz/stats-collector": "^0.1.5",
"@jambonz/realtimedb-helpers": "^0.4.19",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.5",
"aws-sdk": "^2.846.0",
"aws-sdk": "^2.1060.0",
"bent": "^7.3.12",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.1",
"debug": "^4.3.2",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^2.0.13",
"drachtio-srf": "^4.4.55",
"express": "^4.17.1",
"ip": "^1.1.5",
"moment": "^2.29.1",
"parse-url": "^5.0.2",
"pino": "^6.11.2",
"parse-url": "^5.0.7",
"pino": "^6.13.4",
"to-snake-case": "^1.0.0",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.6",

View File

@@ -9,6 +9,7 @@ networks:
services:
mysql:
image: mysql:5.7
platform: linux/x86_64
ports:
- "3360:3306"
environment:
@@ -123,7 +124,7 @@ services:
ipv4_address: 172.38.0.63
influxdb:
image: influxdb:1.8-alpine
image: influxdb:1.8
ports:
- "8086:8086"
networks: