mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-14 10:19:07 +00:00
Compare commits
60 Commits
v0.6.7-rc1
...
feature/sa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2336948cc4 | ||
|
|
caa7b3a03a | ||
|
|
63c0c97024 | ||
|
|
b126719ba7 | ||
|
|
d79c733aa2 | ||
|
|
15f85c9730 | ||
|
|
c1130adf03 | ||
|
|
f982f6c7d8 | ||
|
|
f20190b0fc | ||
|
|
74e85e1b16 | ||
|
|
63e9cb985e | ||
|
|
2e88ab1f55 | ||
|
|
7f75a35515 | ||
|
|
941727e93f | ||
|
|
d8bfa33a00 | ||
|
|
30ed5b6a02 | ||
|
|
bac1b7f2c6 | ||
|
|
48deb3ae89 | ||
|
|
de83f735ea | ||
|
|
cfe9397502 | ||
|
|
dda3335060 | ||
|
|
2329f0cda0 | ||
|
|
36683dc151 | ||
|
|
ce738a7852 | ||
|
|
77a696a0dc | ||
|
|
62ff44540d | ||
|
|
e5821cddf8 | ||
|
|
25567a7842 | ||
|
|
40bd3c9c88 | ||
|
|
27d6d32359 | ||
|
|
142f5d409f | ||
|
|
da4a7184a4 | ||
|
|
2c72bf50cd | ||
|
|
b27f349fc6 | ||
|
|
138aa5836a | ||
|
|
e1a023c21e | ||
|
|
8acb4d1a24 | ||
|
|
26d4bfb63b | ||
|
|
45dcab8517 | ||
|
|
27e3cba00b | ||
|
|
097f36cb00 | ||
|
|
752eed428f | ||
|
|
afb874aabc | ||
|
|
59227febf9 | ||
|
|
8593f12b51 | ||
|
|
3bf1984854 | ||
|
|
0e45e9b27c | ||
|
|
b0a8a6828d | ||
|
|
27d4ad5674 | ||
|
|
d38e77c06c | ||
|
|
c9e2a162c2 | ||
|
|
2b9cb5105f | ||
|
|
afbbed3f5c | ||
|
|
f642967f02 | ||
|
|
fbe2aa2c06 | ||
|
|
5321b5c651 | ||
|
|
83c114803f | ||
|
|
0663174f46 | ||
|
|
3d4359fbe4 | ||
|
|
10382573fa |
51
.github/workflows/docker-publish.yml
vendored
Normal file
51
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: feature-server
|
||||
|
||||
jobs:
|
||||
push:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
3
.husky/pre-push
Executable file
3
.husky/pre-push
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
npm run jslint
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16
|
||||
FROM node:17.4-slim
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
23
app.js
23
app.js
@@ -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);
|
||||
|
||||
@@ -31,6 +31,7 @@ const {
|
||||
|
||||
// HTTP
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const app = express();
|
||||
Object.assign(app.locals, {
|
||||
logger,
|
||||
@@ -73,6 +74,8 @@ srf.invite((req, res) => {
|
||||
});
|
||||
|
||||
// HTTP
|
||||
app.use(helmet());
|
||||
app.use(helmet.hidePoweredBy());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use('/', httpRoutes);
|
||||
@@ -92,6 +95,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);
|
||||
@@ -105,4 +112,16 @@ const disconnect = () => {
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGUSR2', handle);
|
||||
process.on('SIGTERM', handle);
|
||||
|
||||
function handle(signal) {
|
||||
const {removeFromSet} = srf.locals.dbHelpers;
|
||||
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;
|
||||
}
|
||||
|
||||
module.exports = {srf, logger, disconnect};
|
||||
|
||||
29
bin/k8s-pre-stop-hook.js
Executable file
29
bin/k8s-pre-stop-hook.js
Executable 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);
|
||||
}
|
||||
})();
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,7 +8,13 @@ const normalizeJambones = require('./utils/normalize-jambones');
|
||||
const dbUtils = require('./utils/db-utils');
|
||||
|
||||
module.exports = function(srf, logger) {
|
||||
const {lookupAppByPhoneNumber, lookupAppBySid, lookupAppByRealm, lookupAppByTeamsTenant} = srf.locals.dbHelpers;
|
||||
const {
|
||||
lookupAppByPhoneNumber,
|
||||
lookupAppByRegex,
|
||||
lookupAppBySid,
|
||||
lookupAppByRealm,
|
||||
lookupAppByTeamsTenant
|
||||
} = srf.locals.dbHelpers;
|
||||
const {lookupAccountDetails} = dbUtils(logger, srf);
|
||||
function initLocals(req, res, next) {
|
||||
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
|
||||
@@ -112,7 +118,15 @@ module.exports = function(srf, logger) {
|
||||
logger.error(err, `Error retrieving transferred call app for ${arr[1]}`);
|
||||
}
|
||||
}
|
||||
else app = await lookupAppByPhoneNumber(req.locals.calledNumber);
|
||||
else {
|
||||
const voip_carrier_sid = req.get('X-Voip-Carrier-Sid');
|
||||
app = await lookupAppByPhoneNumber(req.locals.calledNumber, voip_carrier_sid);
|
||||
|
||||
if (!app) {
|
||||
/* lookup by call_routes.regex */
|
||||
app = await lookupAppByRegex(req.locals.calledNumber, account_sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!app || !app.call_hook || !app.call_hook.url) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const {CallDirection, CallStatus} = require('../utils/constants');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
/**
|
||||
* @classdesc Represents the common information for all calls
|
||||
* that is provided in call status webhooks
|
||||
@@ -9,6 +8,7 @@ const { v4: uuidv4 } = require('uuid');
|
||||
class CallInfo {
|
||||
constructor(opts) {
|
||||
let from ;
|
||||
let srf;
|
||||
this.direction = opts.direction;
|
||||
if (opts.req) {
|
||||
const u = opts.req.getParsedHeader('from');
|
||||
@@ -19,6 +19,7 @@ class CallInfo {
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
// inbound call
|
||||
const {app, req} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = req.locals.callSid,
|
||||
this.accountSid = app.account_sid,
|
||||
this.applicationSid = app.application_sid;
|
||||
@@ -33,6 +34,7 @@ class CallInfo {
|
||||
else if (opts.parentCallInfo) {
|
||||
// outbound call that is a child of an existing call
|
||||
const {req, parentCallInfo, to, callSid} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = callSid || uuidv4();
|
||||
this.parentCallSid = parentCallInfo.callSid;
|
||||
this.accountSid = parentCallInfo.accountSid;
|
||||
@@ -47,6 +49,7 @@ class CallInfo {
|
||||
else if (this.direction === CallDirection.None) {
|
||||
// outbound SMS
|
||||
const {messageSid, accountSid, applicationSid, res} = opts;
|
||||
srf = res.srf;
|
||||
this.messageSid = messageSid;
|
||||
this.accountSid = accountSid;
|
||||
this.applicationSid = applicationSid;
|
||||
@@ -55,6 +58,7 @@ class CallInfo {
|
||||
else {
|
||||
// outbound call triggered by REST
|
||||
const {req, callSid, accountSid, applicationSid, to, tag} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = callSid;
|
||||
this.accountSid = accountSid;
|
||||
this.applicationSid = applicationSid;
|
||||
@@ -65,6 +69,11 @@ class CallInfo {
|
||||
this.to = to;
|
||||
if (tag) this._customerData = tag;
|
||||
}
|
||||
|
||||
this.localSipAddress = srf.locals.localSipAddress;
|
||||
if (srf.locals.publicIp) {
|
||||
this.publicIp = srf.locals.publicIp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +109,8 @@ class CallInfo {
|
||||
callStatus: this.callStatus,
|
||||
callerId: this.callerId,
|
||||
accountSid: this.accountSid,
|
||||
applicationSid: this.applicationSid
|
||||
applicationSid: this.applicationSid,
|
||||
fsSipAddress: this.localSipAddress
|
||||
};
|
||||
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
|
||||
if (this[prop]) obj[prop] = this[prop];
|
||||
@@ -110,6 +120,13 @@ class CallInfo {
|
||||
if (this._customerData) {
|
||||
Object.assign(obj, {customerData: this._customerData});
|
||||
}
|
||||
|
||||
if (process.env.JAMBONES_API_BASE_URL) {
|
||||
Object.assign(obj, {apiBaseUrl: process.env.JAMBONES_API_BASE_URL});
|
||||
}
|
||||
if (this.publicIp) {
|
||||
Object.assign(obj, {fsPublicIp: this.publicIp});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class CallSession extends Emitter {
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx = 0;
|
||||
this.callGone = false;
|
||||
this.notifiedComplete = false;
|
||||
|
||||
this.tmpFiles = new Set();
|
||||
|
||||
@@ -264,6 +265,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({
|
||||
@@ -632,7 +639,7 @@ class CallSession extends Emitter {
|
||||
try {
|
||||
if (!this.ms) this.ms = this.getMS();
|
||||
const ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
ep.cs = this;
|
||||
//ep.cs = this;
|
||||
this.ep = ep;
|
||||
ep.set({
|
||||
hangup_after_bridge: false,
|
||||
@@ -716,14 +723,12 @@ class CallSession extends Emitter {
|
||||
/**
|
||||
* Hang up the call and free the media endpoint
|
||||
*/
|
||||
async _clearResources() {
|
||||
_clearResources() {
|
||||
for (const resource of [this.dlg, this.ep]) {
|
||||
try {
|
||||
if (resource && resource.connected) await resource.destroy();
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'CallSession:_clearResources error');
|
||||
}
|
||||
if (resource && resource.connected) resource.destroy();
|
||||
}
|
||||
this.dlg = null;
|
||||
this.ep = null;
|
||||
|
||||
// remove any temporary tts files that were created (audio is still cached in redis)
|
||||
for (const path of this.tmpFiles) {
|
||||
@@ -782,9 +787,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');
|
||||
}
|
||||
@@ -955,6 +970,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
|
||||
@@ -967,6 +989,12 @@ class CallSession extends Emitter {
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
if (this.callMoved) return;
|
||||
|
||||
/* race condition: we hang up at the same time as the caller */
|
||||
if (callStatus === CallStatus.Completed) {
|
||||
if (this.notifiedComplete) return;
|
||||
this.notifiedComplete = true;
|
||||
}
|
||||
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
|
||||
@@ -21,15 +21,17 @@ class InboundCallSession extends CallSession {
|
||||
this.req = req;
|
||||
this.res = res;
|
||||
|
||||
req.on('cancel', () => {
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
|
||||
this._callReleased();
|
||||
});
|
||||
req.once('cancel', this._onCancel.bind(this));
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
_onTasksDone() {
|
||||
if (!this.res.finalResponseSent) {
|
||||
if (this._mediaServerFailure) {
|
||||
@@ -45,6 +47,7 @@ class InboundCallSession extends CallSession {
|
||||
this.res.send(603);
|
||||
}
|
||||
}
|
||||
this.req.removeAllListeners('cancel');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +59,7 @@ class InboundCallSession extends CallSession {
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('InboundCallSession: caller hung up');
|
||||
this._callReleased();
|
||||
this.req.removeAllListeners('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
248
lib/tasks/cognigy.js
Normal file
248
lib/tasks/cognigy.js
Normal 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;
|
||||
@@ -453,7 +453,7 @@ class Conference extends Task {
|
||||
this._playSession = null;
|
||||
break;
|
||||
}
|
||||
} while (!this.killed && !this.conf_hold_status === 'hold');
|
||||
} while (!this.killed && this.conf_hold_status !== 'hold');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -147,7 +147,7 @@ class TaskDial extends Task {
|
||||
this.epOther.play(this.dialMusic).catch((err) => {});
|
||||
}
|
||||
}
|
||||
await this._attemptCalls(cs);
|
||||
if (!this.killed) 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);
|
||||
@@ -161,16 +161,25 @@ 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);
|
||||
this.timerMaxCallDuration = null;
|
||||
}
|
||||
if (this.timerRing) {
|
||||
clearTimeout(this.timerRing);
|
||||
this.timerRing = null;
|
||||
}
|
||||
this._removeDtmfDetection(cs.dlg);
|
||||
this._removeDtmfDetection(this.dlg);
|
||||
this._killOutdials();
|
||||
if (this.sd) {
|
||||
this.sd.kill();
|
||||
this.sd.removeAllListeners();
|
||||
this.sd = null;
|
||||
}
|
||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||
@@ -231,10 +240,19 @@ class TaskDial extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_removeHandlers(sd) {
|
||||
sd.removeAllListeners('accept');
|
||||
sd.removeAllListeners('decline');
|
||||
sd.removeAllListeners('adulting');
|
||||
sd.removeAllListeners('callStatusChange');
|
||||
sd.removeAllListeners('callCreateFail');
|
||||
}
|
||||
|
||||
_killOutdials() {
|
||||
for (const [callSid, sd] of Array.from(this.dials)) {
|
||||
this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`);
|
||||
sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`));
|
||||
this._removeHandlers(sd);
|
||||
}
|
||||
this.dials.clear();
|
||||
}
|
||||
@@ -319,8 +337,9 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
const ms = await cs.getMS();
|
||||
const timerRing = setTimeout(() => {
|
||||
this.timerRing = setTimeout(() => {
|
||||
this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`);
|
||||
this.timerRing = null;
|
||||
this._killOutdials();
|
||||
}, this.timeout * 1000);
|
||||
|
||||
@@ -348,6 +367,9 @@ class TaskDial extends Task {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.killed) return;
|
||||
|
||||
const sd = placeCall({
|
||||
logger: this.logger,
|
||||
application: cs.application,
|
||||
@@ -363,7 +385,9 @@ class TaskDial extends Task {
|
||||
|
||||
sd
|
||||
.on('callCreateFail', () => {
|
||||
clearTimeout(this.timerRing);
|
||||
this.dials.delete(sd.callSid);
|
||||
sd.removeAllListeners();
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after call create err, ending task');
|
||||
this.kill(cs);
|
||||
@@ -387,7 +411,8 @@ class TaskDial extends Task {
|
||||
break;
|
||||
case CallStatus.InProgress:
|
||||
this.logger.debug('Dial:_attemptCall -- call was answered');
|
||||
clearTimeout(timerRing);
|
||||
clearTimeout(this.timerRing);
|
||||
this.timerRing = null;
|
||||
break;
|
||||
case CallStatus.Failed:
|
||||
case CallStatus.Busy:
|
||||
@@ -395,7 +420,8 @@ class TaskDial extends Task {
|
||||
this.dials.delete(sd.callSid);
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after call failure, ending task');
|
||||
clearTimeout(timerRing);
|
||||
clearTimeout(this.timerRing);
|
||||
this.timerRing = null;
|
||||
this.kill(cs);
|
||||
}
|
||||
break;
|
||||
@@ -403,6 +429,7 @@ class TaskDial extends Task {
|
||||
})
|
||||
.on('accept', async() => {
|
||||
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
||||
clearTimeout(this.timerRing);
|
||||
try {
|
||||
await this._connectSingleDial(cs, sd);
|
||||
} catch (err) {
|
||||
@@ -411,12 +438,21 @@ class TaskDial extends Task {
|
||||
})
|
||||
.on('decline', () => {
|
||||
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
|
||||
clearTimeout(this.timerRing);
|
||||
this.dials.delete(sd.callSid);
|
||||
sd.removeAllListeners();
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending 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');
|
||||
@@ -448,6 +484,12 @@ class TaskDial extends Task {
|
||||
this._killOutdials(); // NB: order is important
|
||||
}
|
||||
|
||||
_onMaxCallDuration(cs) {
|
||||
this.logger.info(`Dial:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
|
||||
this.ep && this.ep.unbridge();
|
||||
this.kill(cs);
|
||||
}
|
||||
|
||||
/**
|
||||
* We now have a call leg produced by the Dial action, so
|
||||
* - hangup any simrings in progress
|
||||
@@ -468,11 +510,7 @@ class TaskDial extends Task {
|
||||
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 && this.ep.unbridge();
|
||||
this.kill(cs);
|
||||
}, this.timeLimit * 1000);
|
||||
this.timerMaxCallDuration = setTimeout(this._onMaxCallDuration.bind(this, cs), this.timeLimit * 1000);
|
||||
}
|
||||
sessionTracker.add(this.callSid, cs);
|
||||
this.dlg.on('destroy', () => {
|
||||
@@ -523,10 +561,9 @@ class TaskDial extends Task {
|
||||
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)];
|
||||
const bLegSdp = sd.dlg.remote.sdp;
|
||||
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) {
|
||||
@@ -541,6 +578,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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -10,19 +10,25 @@ const {
|
||||
const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
|
||||
const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7);
|
||||
|
||||
class TaskGather extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'finishOnKey', 'hints', 'input', 'numDigits',
|
||||
'partialResultHook',
|
||||
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'submitDigit', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
'retries', 'retryPromptTts', 'retryPromptUrl',
|
||||
'speechTimeout', 'timeout', 'say', 'play'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
|
||||
|
||||
this.timeout = (this.timeout || 5) * 1000;
|
||||
this.interim = this.partialResultCallback;
|
||||
this.logger.debug({opts}, 'created gather task');
|
||||
this.timeout = (this.timeout || 15) * 1000;
|
||||
this.interim = this.partialResultCallback || this.bargein;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
@@ -48,6 +54,12 @@ class TaskGather extends Task {
|
||||
if (this.say) this.sayTask = makeTask(this.logger, {say: this.say}, this);
|
||||
if (this.play) this.playTask = makeTask(this.logger, {play: this.play}, this);
|
||||
|
||||
if (this.sayTask || this.playTask) {
|
||||
// this is specially for barge in where we want to make a bargebale promt
|
||||
// to a user without listening after the say task has finished
|
||||
this.listenAfterSpeech = typeof this.data.listenAfterSpeech === 'boolean' ? this.data.listenAfterSpeech : true;
|
||||
}
|
||||
|
||||
this.parentTask = parentTask;
|
||||
}
|
||||
|
||||
@@ -80,33 +92,63 @@ class TaskGather extends Task {
|
||||
throw new Error(`no speech-to-text service credentials for ${this.vendor} have been configured`);
|
||||
}
|
||||
|
||||
const startListening = (cs, ep) => {
|
||||
this._startTimer();
|
||||
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
||||
this._initSpeech(cs, ep)
|
||||
.then(() => {
|
||||
this._startTranscribing(ep);
|
||||
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.sayTask) {
|
||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.on('playDone', (err) => {
|
||||
if (!this.killed) this._startTimer();
|
||||
this.logger.debug('Gather: kicking off say task');
|
||||
this.sayTask.exec(cs, ep);
|
||||
this.sayTask.on('playDone', async(err) => {
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.debug('Gather: say task completed');
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this.playTask) {
|
||||
this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.on('playDone', (err) => {
|
||||
if (!this.killed) this._startTimer();
|
||||
});
|
||||
this.playTask.on('playDone', async(err) => {
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else this._startTimer();
|
||||
else startListening(cs, ep);
|
||||
|
||||
if (this.input.includes('speech')) {
|
||||
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
||||
await this._initSpeech(cs, ep);
|
||||
this._startTranscribing(ep);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
}
|
||||
|
||||
if (this.input.includes('digits')) {
|
||||
if (this.input.includes('digits') || this.dtmfBargein) {
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
|
||||
await this.awaitTaskDone();
|
||||
this.logger.debug('Gather:exec task has completed');
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'TaskGather:exec error');
|
||||
}
|
||||
@@ -118,6 +160,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
kill(cs) {
|
||||
this.logger.debug('Gather:kill');
|
||||
super.kill(cs);
|
||||
this._killAudio(cs);
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
@@ -126,12 +169,28 @@ class TaskGather extends Task {
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
let resolved = false;
|
||||
if (this.dtmfBargein) this._killAudio(cs);
|
||||
if (evt.dtmf === this.finishOnKey) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-terminator-key');
|
||||
}
|
||||
else {
|
||||
this.digitBuffer += evt.dtmf;
|
||||
if (this.digitBuffer.length === this.numDigits) this._resolve('dtmf-num-digits');
|
||||
const len = this.digitBuffer.length;
|
||||
if (len === this.numDigits || len === this.maxDigits) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-num-digits');
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved && this.interDigitTimeout > 0 && this.digitBuffer.length >= this.minDigits) {
|
||||
/* start interDigitTimer */
|
||||
const ms = this.interDigitTimeout * 1000;
|
||||
this.logger.debug(`starting interdigit timer of ${ms}`);
|
||||
this.interDigitTimer = setTimeout(() => this._resolve('dtmf-interdigit-timeout'), ms);
|
||||
}
|
||||
this._killAudio(cs);
|
||||
}
|
||||
|
||||
async _initSpeech(cs, ep) {
|
||||
@@ -197,7 +256,7 @@ class TaskGather extends Task {
|
||||
ep.startTranscription({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
interim: this.partialResultCallback ? true : false,
|
||||
interim: this.interim,
|
||||
}).catch((err) => {
|
||||
const {writeAlerts, AlertType} = this.cs.srf.locals;
|
||||
this.logger.error(err, 'TaskGather:_startTranscribing error');
|
||||
@@ -237,25 +296,56 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
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;
|
||||
const final = evt.RecognitionStatus === 'Success';
|
||||
if (final) {
|
||||
const nbest = evt.NBest;
|
||||
evt = {
|
||||
is_final: true,
|
||||
alternatives: [
|
||||
{
|
||||
confidence: nbest[0].Confidence,
|
||||
transcript: nbest[0].Display
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
else {
|
||||
evt = {
|
||||
is_final: false,
|
||||
alternatives: [
|
||||
{
|
||||
transcript: evt.Text
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
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'));
|
||||
else {
|
||||
const recognizeSuccess = evt.stability > GATHER_STABILITY_THRESHOLD;
|
||||
/*
|
||||
we need to make sure to only send something on barge in if we have
|
||||
something valid therefore we need to check the recognition
|
||||
stability, which applies to GOOGLE
|
||||
for MS we will have a final event, meaning we will not run into
|
||||
the current if else branch.
|
||||
|
||||
For AWS we still need more testing
|
||||
*/
|
||||
if (recognizeSuccess &&
|
||||
this.bargein &&
|
||||
evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) {
|
||||
this.logger.debug('Gather:_onTranscription - killing audio due to bargein');
|
||||
this._killAudio(cs);
|
||||
this._resolve('speech', evt);
|
||||
}
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
_onEndOfUtterance(cs, ep) {
|
||||
@@ -281,7 +371,8 @@ class TaskGather extends Task {
|
||||
|
||||
this._clearTimer();
|
||||
if (reason.startsWith('dtmf')) {
|
||||
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
if (this.parentTask) this.parentTask.emit('dtmf-collected', {reason, digits: this.digitBuffer});
|
||||
else await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
}
|
||||
else if (reason.startsWith('speech')) {
|
||||
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
||||
|
||||
@@ -64,6 +64,7 @@ class TaskListen extends Task {
|
||||
this.results.dialCallDuration = duration;
|
||||
}
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
this.ep && this._removeListeners(this.ep);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -136,6 +137,10 @@ class TaskListen extends Task {
|
||||
if (this.finishOnKey || this.passDtmf) {
|
||||
ep.removeListener('dtmf', this._dtmfHandler);
|
||||
}
|
||||
ep.removeCustomEventListener(ListenEvents.PlayAudio);
|
||||
ep.removeCustomEventListener(ListenEvents.KillAudio);
|
||||
ep.removeCustomEventListener(ListenEvents.Disconnect);
|
||||
|
||||
}
|
||||
|
||||
_onDtmf(evt) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,15 +21,26 @@ class TaskSay extends Task {
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
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 vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default'
|
||||
? this.synthesizer.vendor
|
||||
: cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default'
|
||||
? this.synthesizer.language
|
||||
: cs.speechSynthesisLanguage ;
|
||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default'
|
||||
? this.synthesizer.voice
|
||||
: cs.speechSynthesisVoice;
|
||||
const engine = this.synthesizer.engine || 'standard';
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
this.logger.info({language, voice}, `Task:say - using vendor: ${vendor}`);
|
||||
this.logger.debug({language,
|
||||
voice,
|
||||
localSynthesizer: this.synthesizer,
|
||||
speechSynthesisVendor: cs.speechSynthesisVendor,
|
||||
speechSynthesisLanguage: cs.speechSynthesisLanguage,
|
||||
speechSynthesisVoice: cs.speechSynthesisVoice
|
||||
}, `Task:say - using vendor: ${vendor}`);
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
@@ -48,6 +59,7 @@ class TaskSay extends Task {
|
||||
vendor,
|
||||
language,
|
||||
voice,
|
||||
engine,
|
||||
salt,
|
||||
credentials
|
||||
}).catch((err) => {
|
||||
@@ -78,7 +90,11 @@ class TaskSay extends Task {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
||||
}
|
||||
else await ep.play(filepath[segment]);
|
||||
else {
|
||||
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
|
||||
await ep.play(filepath[segment]);
|
||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||
}
|
||||
} while (!this.killed && ++segment < filepath.length);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -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",
|
||||
@@ -84,7 +100,15 @@
|
||||
"numDigits": "number",
|
||||
"partialResultHook": "object|string",
|
||||
"speechTimeout": "number",
|
||||
"listenDuringPrompt": "boolean",
|
||||
"bargein": "boolean",
|
||||
"minBargeinWordCount": "number",
|
||||
"dtmfBargein": "boolean",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"timeout": "number",
|
||||
"listenAfterSpeech": "boolean",
|
||||
"recognizer": "#recognizer",
|
||||
"play": "#play",
|
||||
"say": "#say"
|
||||
@@ -308,7 +332,6 @@
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"transcriptionHook",
|
||||
"recognizer"
|
||||
]
|
||||
},
|
||||
@@ -323,13 +346,15 @@
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"headers": "object",
|
||||
"name": "string",
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
"auth": "#auth",
|
||||
"vmail": "boolean",
|
||||
"tenant": "string",
|
||||
"trunk": "string"
|
||||
"trunk": "string",
|
||||
"overrideTo": "string"
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
@@ -353,6 +378,10 @@
|
||||
},
|
||||
"language": "string",
|
||||
"voice": "string",
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"enum": ["standard", "neural"]
|
||||
},
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"enum": ["MALE", "FEMALE", "NEUTRAL"]
|
||||
@@ -369,6 +398,7 @@
|
||||
"enum": ["google", "aws", "microsoft", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"vad": "#vad",
|
||||
"hints": "array",
|
||||
"altLanguages": "array",
|
||||
"profanityFilter": "boolean",
|
||||
@@ -437,5 +467,15 @@
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"vad": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"voiceMs": "number",
|
||||
"mode": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
@@ -21,6 +22,10 @@ class TaskTranscribe extends Task {
|
||||
this.interim = !!recognizer.interim;
|
||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
|
||||
/* google-specific options */
|
||||
this.hints = recognizer.hints || [];
|
||||
this.profanityFilter = recognizer.profanityFilter;
|
||||
@@ -76,6 +81,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);
|
||||
@@ -103,6 +109,12 @@ class TaskTranscribe extends Task {
|
||||
async _startTranscribing(cs, ep) {
|
||||
const opts = {};
|
||||
|
||||
if (this.vad.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||
@@ -210,25 +222,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();
|
||||
|
||||
@@ -45,6 +45,7 @@ class SnsNotifier extends Emitter {
|
||||
}, 'response from SNS SubscribeURL');
|
||||
const data = await this.describeInstance();
|
||||
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState;
|
||||
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
||||
break;
|
||||
|
||||
case 'Notification':
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ function initMS(logger, wrapper, ms) {
|
||||
wrapper.connects = 1;
|
||||
wrapper.active = true;
|
||||
});
|
||||
|
||||
ms.on('channel::open', (evt) => {
|
||||
logger.debug({evt}, `mediaserver ${ms.address} added endpoint`);
|
||||
});
|
||||
ms.on('channel::close', (evt) => {
|
||||
logger.debug({evt}, `mediaserver ${ms.address} removed endpoint`);
|
||||
});
|
||||
}
|
||||
|
||||
function installSrfLocals(srf, logger) {
|
||||
@@ -105,6 +112,7 @@ function installSrfLocals(srf, logger) {
|
||||
const {
|
||||
pool,
|
||||
lookupAppByPhoneNumber,
|
||||
lookupAppByRegex,
|
||||
lookupAppBySid,
|
||||
lookupAppByRealm,
|
||||
lookupAppByTeamsTenant,
|
||||
@@ -159,6 +167,7 @@ function installSrfLocals(srf, logger) {
|
||||
client,
|
||||
pool,
|
||||
lookupAppByPhoneNumber,
|
||||
lookupAppByRegex,
|
||||
lookupAppBySid,
|
||||
lookupAppByRealm,
|
||||
lookupAppByTeamsTenant,
|
||||
|
||||
@@ -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,18 @@ 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,
|
||||
...(this.target.headers || {}),
|
||||
'X-Jambonz-Routing': this.target.type,
|
||||
'X-Call-Sid': this.callSid
|
||||
};
|
||||
if (srf.locals.fsUUID) {
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
};
|
||||
}
|
||||
this.ms = ms;
|
||||
let uri, to;
|
||||
try {
|
||||
@@ -84,7 +90,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 +98,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);
|
||||
@@ -155,6 +150,7 @@ class SingleDialer extends Emitter {
|
||||
* (a) create a CallInfo for this call
|
||||
* (a) create a logger for this call
|
||||
*/
|
||||
req.srf = srf;
|
||||
this.callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
parentCallInfo: this.parentCallInfo,
|
||||
@@ -209,9 +205,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');
|
||||
}
|
||||
@@ -220,6 +222,7 @@ class SingleDialer extends Emitter {
|
||||
if (this.confirmHook) this._executeApp(this.confirmHook);
|
||||
else this.emit('accept');
|
||||
} catch (err) {
|
||||
this.inviteInProgress = null;
|
||||
const status = {callStatus: CallStatus.Failed};
|
||||
if (err instanceof SipError) {
|
||||
status.sipStatus = err.status;
|
||||
@@ -322,9 +325,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'
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -27,6 +28,10 @@ module.exports = (logger) => {
|
||||
lifecycleEmitter = await require('./aws-sns-lifecycle')(logger);
|
||||
|
||||
lifecycleEmitter
|
||||
.on('SubscriptionConfirmation', ({publicIp}) => {
|
||||
const {srf} = require('../..');
|
||||
srf.locals.publicIp = publicIp;
|
||||
})
|
||||
.on(LifeCycleEvents.ScaleIn, () => {
|
||||
logger.info('AWS scale-in notification: begin drying up calls');
|
||||
dryUpCalls = true;
|
||||
@@ -67,7 +72,6 @@ module.exports = (logger) => {
|
||||
})();
|
||||
}
|
||||
|
||||
// send OPTIONS pings to SBCs
|
||||
async function pingProxies(srf) {
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
|
||||
@@ -90,29 +94,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);
|
||||
}, 20000);
|
||||
// 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,
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
30
lib/utils/strip-ancillary-codecs.js
Normal file
30
lib/utils/strip-ancillary-codecs.js
Normal 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;
|
||||
|
||||
1516
package-lock.json
generated
1516
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.6.7-rc8",
|
||||
"version": "v0.7.3",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -23,26 +23,30 @@
|
||||
"start": "node app",
|
||||
"test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
"jslint": "eslint app.js lib",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.6.14",
|
||||
"@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.10",
|
||||
"@jambonz/stats-collector": "^0.1.5",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"aws-sdk": "^2.846.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.26",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"aws-sdk": "^2.1073.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.11",
|
||||
"drachtio-srf": "^4.4.55",
|
||||
"drachtio-fsmrf": "^2.0.13",
|
||||
"drachtio-srf": "^4.4.61",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"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",
|
||||
@@ -53,6 +57,7 @@
|
||||
"clear-module": "^4.1.1",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"husky": "7.0.4",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.2.2"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user