mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
5 Commits
v0.7.4
...
feature/qu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea50e7bdd | ||
|
|
59cf0610dd | ||
|
|
14ab8a9639 | ||
|
|
3ca47f8121 | ||
|
|
8f7b5c959b |
@@ -8,7 +8,7 @@
|
||||
"jsx": false,
|
||||
"modules": false
|
||||
},
|
||||
"ecmaVersion": 2020
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"plugins": ["promise"],
|
||||
"rules": {
|
||||
|
||||
51
.github/workflows/docker-publish.yml
vendored
51
.github/workflows/docker-publish.yml
vendored
@@ -1,51 +0,0 @@
|
||||
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
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,10 +1,16 @@
|
||||
FROM node:17.4-slim
|
||||
FROM node:alpine as builder
|
||||
RUN apk update && apk add --no-cache python make g++
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm prune
|
||||
|
||||
FROM node:alpine as app
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
CMD [ "npm", "start" ]
|
||||
|
||||
36
app.js
36
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 || process.env.K8S, 'missing JAMBONES_SUBNET env var');
|
||||
assert.ok(process.env.JAMBONES_NETWORK_CIDR, '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, FS_UUID_SET_NAME} = require('./lib/utils/constants');
|
||||
const {LifeCycleEvents} = require('./lib/utils/constants');
|
||||
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
||||
installSrfLocals(srf, logger);
|
||||
|
||||
@@ -31,7 +31,6 @@ const {
|
||||
|
||||
// HTTP
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const app = express();
|
||||
Object.assign(app.locals, {
|
||||
logger,
|
||||
@@ -74,8 +73,6 @@ 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);
|
||||
@@ -95,10 +92,6 @@ 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);
|
||||
@@ -112,29 +105,4 @@ 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;
|
||||
}
|
||||
|
||||
if (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS) {
|
||||
const {clearFiles} = require('./lib/utils/cron-jobs');
|
||||
|
||||
/* cleanup orphaned files or channels every so often */
|
||||
setInterval(async() => {
|
||||
try {
|
||||
await clearFiles();
|
||||
} catch (err) {
|
||||
logger.error({err}, 'app.js: error clearing files');
|
||||
}
|
||||
}, 1000 * 60 * (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS || 60));
|
||||
}
|
||||
|
||||
module.exports = {srf, logger, disconnect};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/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);
|
||||
}
|
||||
})();
|
||||
@@ -6,8 +6,7 @@ const {CallDirection, CallStatus} = require('../../utils/constants');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const SipError = require('drachtio-srf').SipError;
|
||||
const sysError = require('./error');
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
const WsRequestor = require('../../utils/ws-requestor');
|
||||
const Requestor = require('../../utils/requestor');
|
||||
const dbUtils = require('../../utils/db-utils');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
@@ -36,8 +35,6 @@ 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
|
||||
};
|
||||
@@ -60,11 +57,6 @@ router.post('/', async(req, res) => {
|
||||
case 'user':
|
||||
uri = `sip:${target.name}`;
|
||||
to = target.name;
|
||||
if (target.overrideTo) {
|
||||
Object.assign(opts.headers, {
|
||||
'X-Override-To': target.overrideTo
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'sip':
|
||||
uri = target.sipUri;
|
||||
@@ -72,17 +64,6 @@ router.post('/', async(req, res) => {
|
||||
break;
|
||||
}
|
||||
|
||||
if (target.type === 'phone' && target.trunk) {
|
||||
const {lookupCarrier} = dbUtils(this.logger, srf);
|
||||
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
||||
this.logger.info(
|
||||
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
||||
if (voip_carrier_sid) {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* create endpoint for outdial */
|
||||
const ms = getFreeswitch();
|
||||
if (!ms) throw new Error('no available Freeswitch for outbound call creation');
|
||||
@@ -116,30 +97,21 @@ router.post('/', async(req, res) => {
|
||||
* attach our requestor and notifier objects
|
||||
* these will be used for all http requests we make during this call
|
||||
*/
|
||||
if ('WS' === app.call_hook?.method) {
|
||||
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook,
|
||||
account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
app.requestor = new Requestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||
if (app.call_status_hook) {
|
||||
app.notifier = new Requestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
||||
}
|
||||
else app.notifier = {request: () => {}};
|
||||
|
||||
/* now launch the outdial */
|
||||
try {
|
||||
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
||||
const dlg = await srf.createUAC(uri, opts, {
|
||||
cbRequest: (err, inviteReq) => {
|
||||
/* in case of 302 redirect, this gets called twice, ignore the second */
|
||||
if (res.headersSent) return;
|
||||
|
||||
if (err) {
|
||||
logger.error(err, 'createCall Error creating call');
|
||||
res.status(500).send('Call Failure');
|
||||
return;
|
||||
}
|
||||
inviteReq.srf = srf;
|
||||
/* ok our outbound INVITE is in flight */
|
||||
|
||||
const tasks = [restDial];
|
||||
@@ -171,11 +143,7 @@ router.post('/', async(req, res) => {
|
||||
}
|
||||
});
|
||||
connectStream(dlg.remote.sdp);
|
||||
cs.emit('callStatusChange', {
|
||||
callStatus: CallStatus.InProgress,
|
||||
sipStatus: 200,
|
||||
sipReason: 'OK'
|
||||
});
|
||||
cs.emit('callStatusChange', {callStatus: CallStatus.InProgress, sipStatus: 200});
|
||||
restDial.emit('callStatus', 200);
|
||||
restDial.emit('connect', dlg);
|
||||
}
|
||||
@@ -186,18 +154,10 @@ router.post('/', async(req, res) => {
|
||||
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
|
||||
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
|
||||
else console.log(`REST outdial failed with ${err.status}`);
|
||||
if (cs) cs.emit('callStatusChange', {
|
||||
callStatus,
|
||||
sipStatus: err.status,
|
||||
sipReason: err.reason
|
||||
});
|
||||
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: err.status});
|
||||
}
|
||||
else {
|
||||
if (cs) cs.emit('callStatusChange', {
|
||||
callStatus,
|
||||
sipStatus: 500,
|
||||
sipReason: 'Internal Server Error'
|
||||
});
|
||||
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: 500});
|
||||
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
|
||||
else console.error(err);
|
||||
}
|
||||
|
||||
@@ -9,4 +9,8 @@ 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,6 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
const WsRequestor = require('../../utils/ws-requestor');
|
||||
const Requestor = require('../../utils/requestor');
|
||||
const CallInfo = require('../../session/call-info');
|
||||
const {CallDirection} = require('../../utils/constants');
|
||||
const SmsSession = require('../../session/sms-call-session');
|
||||
@@ -19,17 +18,7 @@ router.post('/:partner', async(req, res) => {
|
||||
const app = req.body.app;
|
||||
const account = await lookupAccountBySid(app.accountSid);
|
||||
const hook = app.messaging_hook;
|
||||
let requestor;
|
||||
|
||||
if ('WS' === hook?.method) {
|
||||
app.requestor = new WsRequestor(logger, account.account_sid, hook, account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account.account_sid, hook, account.webhook_secret);
|
||||
app.notifier = {request: () => {}};
|
||||
}
|
||||
|
||||
const requestor = new Requestor(logger, account.account_sid, hook, account.webhook_secret);
|
||||
const payload = {
|
||||
carrier: req.params.partner,
|
||||
messageSid: app.messageSid,
|
||||
@@ -44,7 +33,7 @@ router.post('/:partner', async(req, res) => {
|
||||
res.status(200).json({sid: req.body.messageSid});
|
||||
|
||||
try {
|
||||
tasks = await requestor.request('session:new', hook, payload);
|
||||
tasks = await requestor.request(hook, payload);
|
||||
logger.info({tasks}, 'response from incoming SMS webhook');
|
||||
} catch (err) {
|
||||
logger.error({err, hook}, 'Error sending incoming SMS message');
|
||||
|
||||
@@ -12,9 +12,6 @@ function retrieveCallSession(callSid, opts) {
|
||||
throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated');
|
||||
}
|
||||
const cs = sessionTracker.get(callSid);
|
||||
if (!cs) {
|
||||
throw new DbErrorUnprocessableRequest('call session is gone');
|
||||
}
|
||||
|
||||
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
|
||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
||||
@@ -48,18 +45,8 @@ router.post('/:callSid', async(req, res) => {
|
||||
logger.info(`updateCall: callSid not found ${callSid}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
if (req.body.sip_request) {
|
||||
const response = await cs.updateCall(req.body, callSid);
|
||||
res.status(200).json({
|
||||
status: response.status,
|
||||
reason: response.reason
|
||||
});
|
||||
}
|
||||
else {
|
||||
res.sendStatus(202);
|
||||
cs.updateCall(req.body, callSid);
|
||||
}
|
||||
res.sendStatus(202);
|
||||
cs.updateCall(req.body, callSid);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
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 check
|
||||
routes.get('/health', readiness);
|
||||
// health checks
|
||||
routes.get('/', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
routes.get('/health', (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
module.exports = routes;
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const {CallDirection} = require('./utils/constants');
|
||||
const CallInfo = require('./session/call-info');
|
||||
const HttpRequestor = require('./utils/http-requestor');
|
||||
const WsRequestor = require('./utils/ws-requestor');
|
||||
const Requestor = require('./utils/requestor');
|
||||
const makeTask = require('./tasks/make_task');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const normalizeJambones = require('./utils/normalize-jambones');
|
||||
const dbUtils = require('./utils/db-utils');
|
||||
|
||||
module.exports = function(srf, logger) {
|
||||
const {
|
||||
lookupAppByPhoneNumber,
|
||||
lookupAppByRegex,
|
||||
lookupAppBySid,
|
||||
lookupAppByRealm,
|
||||
lookupAppByTeamsTenant
|
||||
} = srf.locals.dbHelpers;
|
||||
const {lookupAppByPhoneNumber, 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();
|
||||
@@ -52,7 +45,7 @@ module.exports = function(srf, logger) {
|
||||
// TODO: alert
|
||||
return res.send(503, {headers: {'X-Reason': 'Account exists but is inactive'}});
|
||||
}
|
||||
logger.debug({accountInfo: req.locals?.accountInfo?.account}, `retrieved account info for ${account_sid}`);
|
||||
logger.debug({accountInfo: req.locals.accountInfo}, `retrieved account info for ${account_sid}`);
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info({err}, `Error retrieving account details for account ${account_sid}`);
|
||||
@@ -106,7 +99,7 @@ module.exports = function(srf, logger) {
|
||||
}
|
||||
else {
|
||||
const uri = parseUri(req.uri);
|
||||
const arr = /context-(.*)/.exec(uri?.user);
|
||||
const arr = /context-(.*)/.exec(uri.user);
|
||||
if (arr) {
|
||||
// this is a transfer from another feature server
|
||||
const {retrieveKey, deleteKey} = srf.locals.dbHelpers;
|
||||
@@ -119,15 +112,7 @@ module.exports = function(srf, logger) {
|
||||
logger.error(err, `Error retrieving transferred call app for ${arr[1]}`);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
else app = await lookupAppByPhoneNumber(req.locals.calledNumber);
|
||||
}
|
||||
|
||||
if (!app || !app.call_hook || !app.call_hook.url) {
|
||||
@@ -143,26 +128,16 @@ module.exports = function(srf, logger) {
|
||||
* create a requestor that we will use for all http requests we make during the call.
|
||||
* also create a notifier for call status events (if not needed, its a no-op).
|
||||
*/
|
||||
if ('WS' === app.call_hook?.method ||
|
||||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
|
||||
app.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
app.call_hook.method = 'WS';
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
|
||||
accountInfo.account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
}
|
||||
app.requestor = new Requestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new Requestor(logger, account_sid, app.call_status_hook,
|
||||
accountInfo.account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
|
||||
req.locals.application = app;
|
||||
const obj = Object.assign({}, app);
|
||||
delete obj.requestor;
|
||||
delete obj.notifier;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {call_hook, call_status_hook, ...appInfo} = obj; // mask sensitive data like user/pass on webhook
|
||||
logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
||||
logger.info({app: obj}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
||||
req.locals.callInfo = new CallInfo({req, app, direction: CallDirection.Inbound});
|
||||
next();
|
||||
} catch (err) {
|
||||
@@ -185,16 +160,15 @@ module.exports = function(srf, logger) {
|
||||
return next();
|
||||
}
|
||||
/* retrieve the application to execute for this inbound call */
|
||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
|
||||
const params = Object.assign(app.call_hook.method === 'POST' ? {sip: req.msg} : {},
|
||||
req.locals.callInfo);
|
||||
const json = await app.requestor.request('session:new', app.call_hook, params);
|
||||
const json = await app.requestor.request(app.call_hook, params);
|
||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err?.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}});
|
||||
app.requestor.close();
|
||||
logger.info(`Error retrieving or parsing application: ${err.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err.message}});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@ const CallSession = require('./call-session');
|
||||
|
||||
*/
|
||||
class AdultingCallSession extends CallSession {
|
||||
constructor({logger, application, singleDialer, tasks, callInfo, accountInfo}) {
|
||||
constructor({logger, application, singleDialer, tasks, callInfo}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
srf: singleDialer.dlg.srf,
|
||||
tasks,
|
||||
callInfo,
|
||||
accountInfo
|
||||
callInfo
|
||||
});
|
||||
this.sd = singleDialer;
|
||||
|
||||
@@ -30,25 +29,15 @@ class AdultingCallSession extends CallSession {
|
||||
return this.sd.dlg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this is not an error. It is only here to avoid an assert ("no setter for dlg")
|
||||
* when there is a call in Session:_clearResources to null out dlg and ep
|
||||
*/
|
||||
set dlg(newDlg) {}
|
||||
|
||||
get ep() {
|
||||
return this.sd.ep;
|
||||
}
|
||||
|
||||
/* see note above */
|
||||
set ep(newEp) {}
|
||||
|
||||
get callSid() {
|
||||
return this.callInfo.callSid;
|
||||
}
|
||||
|
||||
_callerHungup() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = AdultingCallSession;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -8,7 +9,6 @@ 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,7 +19,6 @@ 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;
|
||||
@@ -27,7 +26,6 @@ class CallInfo {
|
||||
this.to = req.calledNumber;
|
||||
this.callId = req.get('Call-ID');
|
||||
this.sipStatus = 100;
|
||||
this.sipReason = 'Trying';
|
||||
this.callStatus = CallStatus.Trying;
|
||||
this.originatingSipIp = req.get('X-Forwarded-For');
|
||||
this.originatingSipTrunkName = req.get('X-Originating-Carrier');
|
||||
@@ -35,7 +33,6 @@ 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;
|
||||
@@ -46,12 +43,10 @@ class CallInfo {
|
||||
this.callId = req.get('Call-ID');
|
||||
this.callStatus = CallStatus.Trying,
|
||||
this.sipStatus = 100;
|
||||
this.sipReason = 'Trying';
|
||||
}
|
||||
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;
|
||||
@@ -60,23 +55,16 @@ 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;
|
||||
this.callStatus = CallStatus.Trying,
|
||||
this.callId = req.get('Call-ID');
|
||||
this.sipStatus = 100;
|
||||
this.sipReason = 'Trying';
|
||||
this.from = from || req.callingNumber;
|
||||
this.to = to;
|
||||
if (tag) this._customerData = tag;
|
||||
}
|
||||
|
||||
this.localSipAddress = srf.locals.localSipAddress;
|
||||
if (srf.locals.publicIp) {
|
||||
this.publicIp = srf.locals.publicIp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,10 +72,9 @@ class CallInfo {
|
||||
* @param {string} callStatus - current call status
|
||||
* @param {number} sipStatus - current sip status
|
||||
*/
|
||||
updateCallStatus(callStatus, sipStatus, sipReason) {
|
||||
updateCallStatus(callStatus, sipStatus) {
|
||||
this.callStatus = callStatus;
|
||||
if (sipStatus) this.sipStatus = sipStatus;
|
||||
if (sipReason) this.sipReason = sipReason;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,12 +97,10 @@ class CallInfo {
|
||||
to: this.to,
|
||||
callId: this.callId,
|
||||
sipStatus: this.sipStatus,
|
||||
sipReason: this.sipReason,
|
||||
callStatus: this.callStatus,
|
||||
callerId: this.callerId,
|
||||
accountSid: this.accountSid,
|
||||
applicationSid: this.applicationSid,
|
||||
fsSipAddress: this.localSipAddress
|
||||
applicationSid: this.applicationSid
|
||||
};
|
||||
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
|
||||
if (this[prop]) obj[prop] = this[prop];
|
||||
@@ -125,13 +110,6 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ const sessionTracker = require('./session-tracker');
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const listTaskNames = require('../utils/summarize-tasks');
|
||||
const HttpRequestor = require('../utils/http-requestor');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
const Requestor = require('../utils/requestor');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
||||
|
||||
@@ -35,7 +34,7 @@ class CallSession extends Emitter {
|
||||
* @param {array} opts.tasks - tasks we are to execute
|
||||
* @param {callInfo} opts.callInfo - information about the call
|
||||
*/
|
||||
constructor({logger, application, srf, tasks, callInfo, accountInfo, memberId, confName, confUuid}) {
|
||||
constructor({logger, application, srf, tasks, callInfo, accountInfo}) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.application = application;
|
||||
@@ -43,13 +42,9 @@ class CallSession extends Emitter {
|
||||
this.callInfo = callInfo;
|
||||
this.accountInfo = accountInfo;
|
||||
this.tasks = tasks;
|
||||
this.memberId = memberId;
|
||||
this.confName = confName;
|
||||
this.confUuid = confUuid;
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx = 0;
|
||||
this.callGone = false;
|
||||
this.notifiedComplete = false;
|
||||
|
||||
this.tmpFiles = new Set();
|
||||
|
||||
@@ -63,8 +58,6 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
this._pool = srf.locals.dbHelpers.pool;
|
||||
|
||||
this.requestor.on('command', this._onCommand.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,27 +110,18 @@ class CallSession extends Emitter {
|
||||
get speechSynthesisVendor() {
|
||||
return this.application.speech_synthesis_vendor;
|
||||
}
|
||||
set speechSynthesisVendor(vendor) {
|
||||
this.application.speech_synthesis_vendor = vendor;
|
||||
}
|
||||
/**
|
||||
* default voice to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
get speechSynthesisVoice() {
|
||||
return this.application.speech_synthesis_voice;
|
||||
}
|
||||
set speechSynthesisVoice(voice) {
|
||||
this.application.speech_synthesis_voice = voice;
|
||||
}
|
||||
/**
|
||||
* default language to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
get speechSynthesisLanguage() {
|
||||
return this.application.speech_synthesis_language;
|
||||
}
|
||||
set speechSynthesisLanguage(language) {
|
||||
this.application.speech_synthesis_language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* default vendor to use for speech recognition if not provided in the app
|
||||
@@ -145,18 +129,12 @@ class CallSession extends Emitter {
|
||||
get speechRecognizerVendor() {
|
||||
return this.application.speech_recognizer_vendor;
|
||||
}
|
||||
set speechRecognizerVendor(vendor) {
|
||||
this.application.speech_recognizer_vendor = vendor;
|
||||
}
|
||||
/**
|
||||
* default language to use for speech recognition if not provided in the app
|
||||
*/
|
||||
get speechRecognizerLanguage() {
|
||||
return this.application.speech_recognizer_language;
|
||||
}
|
||||
set speechRecognizerLanguage(language) {
|
||||
this.application.speech_recognizer_language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* indicates whether the call currently in progress
|
||||
@@ -214,77 +192,13 @@ class CallSession extends Emitter {
|
||||
return this.constructor.name === 'SmsCallSession';
|
||||
}
|
||||
|
||||
get webhook_secret() {
|
||||
return this.accountInfo?.account?.webhook_secret;
|
||||
}
|
||||
|
||||
get isInConference() {
|
||||
return this.memberId && this.confName && this.confUuid;
|
||||
}
|
||||
|
||||
get isBotModeEnabled() {
|
||||
return this.backgroundGatherTask;
|
||||
}
|
||||
|
||||
async enableBotMode(gather) {
|
||||
try {
|
||||
const t = normalizeJambones(this.logger, [gather]);
|
||||
this.backgroundGatherTask = makeTask(this.logger, t[0]);
|
||||
this.backgroundGatherTask
|
||||
.on('dtmf', this._clearTasks.bind(this))
|
||||
.on('transcription', this._clearTasks.bind(this))
|
||||
.on('timeout', this._clearTasks.bind(this));
|
||||
this.logger.info({gather}, 'CallSession:enableBotMode - starting background gather');
|
||||
const resources = await this._evaluatePreconditions(this.backgroundGatherTask);
|
||||
this.backgroundGatherTask.exec(this, resources)
|
||||
.then(() => {
|
||||
this.logger.info('CallSession:enableBotMode: gather completed');
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask = null;
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.info({err}, 'CallSession:enableBotMode: gather threw error');
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask = null;
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.info({err, gather}, 'CallSession:enableBotMode - Error creating gather task');
|
||||
}
|
||||
}
|
||||
disableBotMode() {
|
||||
if (this.backgroundGatherTask) {
|
||||
try {
|
||||
this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask.kill();
|
||||
} catch (err) {}
|
||||
this.backgroundGatherTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
setConferenceDetails(memberId, confName, confUuid) {
|
||||
assert(!this.memberId && !this.confName && !this.confUuid);
|
||||
assert (memberId && confName && confUuid);
|
||||
|
||||
this.logger.debug(`session is now in conference ${confName}:${memberId} - uuid ${confUuid}`);
|
||||
this.memberId = memberId;
|
||||
this.confName = confName;
|
||||
this.confUuid = confUuid;
|
||||
}
|
||||
|
||||
clearConferenceDetails() {
|
||||
this.logger.debug(`session has now left conference ${this.confName}:${this.memberId}`);
|
||||
this.memberId = null;
|
||||
this.confName = null;
|
||||
this.confUuid = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for speech credentials for the specified vendor
|
||||
* @param {*} vendor - google or aws
|
||||
*/
|
||||
getSpeechCredentials(vendor, type) {
|
||||
const {writeAlerts, AlertType} = this.srf.locals;
|
||||
this.logger.debug({vendor, type, speech: this.accountInfo.speech}, `searching for speech for vendor ${vendor}`);
|
||||
if (this.accountInfo.speech && this.accountInfo.speech.length > 0) {
|
||||
const credential = this.accountInfo.speech.find((s) => s.vendor === vendor);
|
||||
if (credential && (
|
||||
@@ -316,19 +230,6 @@ class CallSession extends Emitter {
|
||||
region: process.env.AWS_REGION || credential.aws_region
|
||||
};
|
||||
}
|
||||
else if ('microsoft' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key,
|
||||
region: credential.region
|
||||
};
|
||||
}
|
||||
else if ('wellsaid' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
writeAlerts({
|
||||
@@ -347,7 +248,6 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
async exec() {
|
||||
this.logger.info({tasks: listTaskNames(this.tasks)}, `CallSession:exec starting ${this.tasks.length} tasks`);
|
||||
|
||||
while (this.tasks.length && !this.callGone) {
|
||||
const taskNum = ++this.taskIdx;
|
||||
const stackNum = this.stackIdx;
|
||||
@@ -356,19 +256,12 @@ class CallSession extends Emitter {
|
||||
try {
|
||||
const resources = await this._evaluatePreconditions(task);
|
||||
this.currentTask = task;
|
||||
if (TaskName.Gather === task.name && this.isBotModeEnabled) {
|
||||
const timeout = task.timeout;
|
||||
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
|
||||
this.backgroundGatherTask.updateTimeout(timeout);
|
||||
}
|
||||
else {
|
||||
await task.exec(this, resources);
|
||||
}
|
||||
await task.exec(this, resources);
|
||||
this.currentTask = null;
|
||||
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
} catch (err) {
|
||||
this.currentTask = null;
|
||||
if (err.message?.includes(BADPRECONDITIONS)) {
|
||||
if (err.message.includes(BADPRECONDITIONS)) {
|
||||
this.logger.info(`CallSession:exec task #${stackNum}:${taskNum}: ${task.name}: ${err.message}`);
|
||||
}
|
||||
else {
|
||||
@@ -376,16 +269,6 @@ class CallSession extends Emitter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === this.tasks.length && this.hasStableDialog && this.requestor instanceof WsRequestor) {
|
||||
try {
|
||||
await this._awaitCommandsOrHangup();
|
||||
if (!this.hasStableDialog || this.callGone) break;
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// all done - cleanup
|
||||
@@ -444,11 +327,6 @@ class CallSession extends Emitter {
|
||||
this.currentTask.kill(this);
|
||||
this.currentTask = null;
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
this.requestor && this.requestor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,7 +336,7 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
_lccCallStatus(opts) {
|
||||
if (opts.call_status === CallStatus.Completed && this.dlg) {
|
||||
this.logger.info('CallSession:_lccCallStatus hanging up call due to request from api');
|
||||
this.logger.info('CallSession:updateCall hanging up call due to request from api');
|
||||
this._callerHungup();
|
||||
}
|
||||
else if (opts.call_status === CallStatus.NoAnswer) {
|
||||
@@ -485,42 +363,29 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
async _lccCallHook(opts) {
|
||||
const webhooks = [];
|
||||
let sd, tasks, childTasks;
|
||||
|
||||
if (opts.call_hook || opts.child_call_hook) {
|
||||
if (opts.call_hook) {
|
||||
webhooks.push(this.requestor.request('session:redirect', opts.call_hook, this.callInfo.toJSON()));
|
||||
let sd;
|
||||
if (opts.call_hook) webhooks.push(this.requestor.request(opts.call_hook, this.callInfo));
|
||||
if (opts.child_call_hook) {
|
||||
/* child call hook only allowed from a connected Dial state */
|
||||
const task = this.currentTask;
|
||||
sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
webhooks.push(this.requestor.request(opts.child_call_hook, sd.callInfo));
|
||||
}
|
||||
if (opts.child_call_hook) {
|
||||
/* child call hook only allowed from a connected Dial state */
|
||||
const task = this.currentTask;
|
||||
sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
webhooks.push(this.requestor.request('session:redirect', opts.child_call_hook, sd.callInfo.toJSON()));
|
||||
}
|
||||
}
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
if (opts.call_hook) {
|
||||
tasks = tasks1;
|
||||
if (opts.child_call_hook) childTasks = tasks2;
|
||||
}
|
||||
else childTasks = tasks1;
|
||||
}
|
||||
else if (opts.parent_call || opts.child_call) {
|
||||
const {parent_call, child_call} = opts;
|
||||
assert.ok(!parent_call || Array.isArray(parent_call), 'CallSession:_lccCallHook - parent_call must be an array');
|
||||
assert.ok(!child_call || Array.isArray(child_call), 'CallSession:_lccCallHook - child_call must be an array');
|
||||
tasks = parent_call;
|
||||
childTasks = child_call;
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
let tasks, childTasks;
|
||||
if (opts.call_hook) {
|
||||
tasks = tasks1;
|
||||
if (opts.child_call_hook) childTasks = tasks2;
|
||||
}
|
||||
else childTasks = tasks1;
|
||||
|
||||
if (childTasks) {
|
||||
const {parentLogger} = this.srf.locals;
|
||||
const childLogger = parentLogger.child({callId: this.callId, callSid: sd.callSid});
|
||||
const t = normalizeJambones(childLogger, childTasks).map((tdata) => makeTask(childLogger, tdata));
|
||||
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list for child call');
|
||||
|
||||
// TODO: if using websockets api, we need a new websocket for the adulting session..
|
||||
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list for child call');
|
||||
const cs = await sd.doAdulting({
|
||||
logger: childLogger,
|
||||
application: this.application,
|
||||
@@ -532,7 +397,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
if (tasks) {
|
||||
const t = normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata));
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list');
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list');
|
||||
this.replaceApplication(t);
|
||||
}
|
||||
else {
|
||||
@@ -549,63 +414,23 @@ class CallSession extends Emitter {
|
||||
async _lccListenStatus(opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || ![TaskName.Dial, TaskName.Listen].includes(task.name)) {
|
||||
return this.logger.info(`CallSession:_lccListenStatus - invalid listen_status in task ${task.name}`);
|
||||
return this.logger.info(`CallSession:updateCall - invalid listen_status in task ${task.name}`);
|
||||
}
|
||||
const listenTask = task.name === TaskName.Listen ? task : task.listenTask;
|
||||
if (!listenTask) {
|
||||
return this.logger.info('CallSession:_lccListenStatus - invalid listen_status: Dial does not have a listen');
|
||||
return this.logger.info('CallSession:updateCall - invalid listen_status: Dial does not have a listen');
|
||||
}
|
||||
listenTask.updateListen(opts.listen_status);
|
||||
}
|
||||
|
||||
async _lccMuteStatus(callSid, mute) {
|
||||
// this whole thing requires us to be in a Dial or Conference verb
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
const task = this.currentTask;
|
||||
if (!task || ![TaskName.Dial, TaskName.Conference].includes(task.name)) {
|
||||
return this.logger.info('CallSession:_lccMuteStatus - invalid: neither dial nor conference are not active');
|
||||
}
|
||||
// now do the mute/unmute
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
|
||||
}
|
||||
|
||||
async _lccConfHoldStatus(opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
|
||||
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
|
||||
}
|
||||
task.doConferenceHold(this, opts);
|
||||
}
|
||||
|
||||
async _lccConfMuteStatus(opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
|
||||
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
|
||||
}
|
||||
task.doConferenceMuteNonModerators(this, opts);
|
||||
}
|
||||
|
||||
async _lccSipRequest(opts, callSid) {
|
||||
const {sip_request} = opts;
|
||||
const {method, content_type, content, headers = {}} = sip_request;
|
||||
if (!this.hasStableDialog) {
|
||||
this.logger.info('CallSession:_lccSipRequest - invalid command as we do not have a stable call');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dlg = callSid === this.callSid ? this.dlg : this.currentTask.dlg;
|
||||
const res = await dlg.request({
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': content_type
|
||||
},
|
||||
body: content
|
||||
});
|
||||
this.logger.debug({res}, `CallSession:_lccSipRequest got response to ${method}`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `CallSession:_lccSipRequest - error sending ${method}`);
|
||||
if (!task || TaskName.Dial !== task.name) {
|
||||
return this.logger.info('CallSession:_lccMute - invalid command as dial is not active');
|
||||
}
|
||||
// now do the whisper
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -625,7 +450,7 @@ class CallSession extends Emitter {
|
||||
// allow user to provide a url object, a url string, an array of tasks, or a single task
|
||||
if (typeof whisper === 'string' || (typeof whisper === 'object' && whisper.url)) {
|
||||
// retrieve a url
|
||||
const json = await this.requestor(opts.call_hook, this.callInfo.toJSON());
|
||||
const json = await this.requestor(opts.call_hook, this.callInfo);
|
||||
tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
}
|
||||
else if (Array.isArray(whisper)) {
|
||||
@@ -656,6 +481,20 @@ class CallSession extends Emitter {
|
||||
task.whisper(tasks, callSid).catch((err) => this.logger.error(err, 'CallSession:_lccWhisper'));
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- mute or unmute an endpoint
|
||||
* @param {array} opts - array of play or say tasks
|
||||
*/
|
||||
async _lccMute(callSid, mute) {
|
||||
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Dial !== task.name) {
|
||||
return this.logger.info('CallSession:_lccMute - not possible since we are not in a dial');
|
||||
}
|
||||
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control
|
||||
@@ -677,16 +516,6 @@ class CallSession extends Emitter {
|
||||
else if (opts.mute_status) {
|
||||
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
|
||||
}
|
||||
else if (opts.conf_hold_status) {
|
||||
await this._lccConfHoldStatus(opts);
|
||||
}
|
||||
else if (opts.conf_mute_status) {
|
||||
await this._lccConfMuteStatus(opts);
|
||||
}
|
||||
else if (opts.sip_request) {
|
||||
const res = await this._lccSipRequest(opts, callSid);
|
||||
return {status: res.status, reason: res.reason};
|
||||
}
|
||||
|
||||
// whisper may be the only thing we are asked to do, or it may that
|
||||
// we are doing a whisper after having muted, paused reccording etc..
|
||||
@@ -726,67 +555,6 @@ class CallSession extends Emitter {
|
||||
this.taskIdx = 0;
|
||||
}
|
||||
|
||||
_onCommand({msgid, command, call_sid, queueCommand, data}) {
|
||||
this.logger.info({msgid, command, queueCommand}, 'CallSession:_onCommand - received command');
|
||||
switch (command) {
|
||||
case 'redirect':
|
||||
if (Array.isArray(data)) {
|
||||
const t = normalizeJambones(this.logger, data)
|
||||
.map((tdata) => makeTask(this.logger, tdata));
|
||||
if (!queueCommand) {
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list');
|
||||
this.replaceApplication(t);
|
||||
}
|
||||
else {
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
|
||||
this.tasks.push(...t);
|
||||
this.logger.debug({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||
}
|
||||
}
|
||||
else this._lccCallHook(data);
|
||||
break;
|
||||
|
||||
case 'call:status':
|
||||
this._lccCallStatus(data);
|
||||
break;
|
||||
|
||||
case 'mute:status':
|
||||
this._lccMuteStatus(call_sid, data);
|
||||
break;
|
||||
|
||||
case 'conf:mute-status':
|
||||
this._lccConfMuteStatus(data);
|
||||
break;
|
||||
|
||||
case 'conf:hold-status':
|
||||
this._lccConfHoldStatus(data);
|
||||
break;
|
||||
|
||||
case 'listen:status':
|
||||
this._lccListenStatus(data);
|
||||
break;
|
||||
|
||||
case 'whisper':
|
||||
this._lccWhisper(data, call_sid);
|
||||
break;
|
||||
|
||||
case 'sip:request':
|
||||
this._lccSipRequest(data, call_sid)
|
||||
.catch((err) => {
|
||||
this.logger.info({err, data}, `CallSession:_onCommand - error sending ${data.method}`);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.logger.info('CallSession:_onCommand - got commands, waking up..');
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
|
||||
_evaluatePreconditions(task) {
|
||||
switch (task.preconditions) {
|
||||
case TaskPreconditions.None:
|
||||
@@ -822,7 +590,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,
|
||||
@@ -851,11 +619,7 @@ class CallSession extends Emitter {
|
||||
} catch (err) {
|
||||
if (err === CALLER_CANCELLED_ERR_MSG) {
|
||||
this.logger.error(err, 'caller canceled quickly before we could respond, ending call');
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.NoAnswer,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Request Terminated'
|
||||
});
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
|
||||
this._callReleased();
|
||||
}
|
||||
else {
|
||||
@@ -910,12 +674,14 @@ class CallSession extends Emitter {
|
||||
/**
|
||||
* Hang up the call and free the media endpoint
|
||||
*/
|
||||
_clearResources() {
|
||||
async _clearResources() {
|
||||
for (const resource of [this.dlg, this.ep]) {
|
||||
if (resource && resource.connected) resource.destroy();
|
||||
try {
|
||||
if (resource && resource.connected) await resource.destroy();
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'CallSession:_clearResources error');
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -927,7 +693,6 @@ class CallSession extends Emitter {
|
||||
});
|
||||
}
|
||||
this.tmpFiles.clear();
|
||||
this.requestor && this.requestor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -965,10 +730,9 @@ class CallSession extends Emitter {
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.wrapDialog(this.dlg);
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.emit('callStatusChange', {sipStatus: 200, sipReason: 'OK', callStatus: CallStatus.InProgress});
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
|
||||
this.dlg.on('modify', this._onReinvite.bind(this));
|
||||
this.dlg.on('refer', this._onRefer.bind(this));
|
||||
|
||||
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
|
||||
}
|
||||
@@ -976,40 +740,14 @@ class CallSession extends Emitter {
|
||||
|
||||
async _onReinvite(req, res) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming REFER if we are in a dial task
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
_onRefer(req, res) {
|
||||
const task = this.currentTask;
|
||||
const sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
task.handleRefer(this, req, res);
|
||||
}
|
||||
else {
|
||||
res.send(501);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create and endpoint if we don't have one; otherwise simply return
|
||||
* the current media server and endpoint that are associated with this call
|
||||
@@ -1046,8 +784,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
else {
|
||||
this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found');
|
||||
this.queueEventHookRequestor = new HttpRequestor(this.logger, this.accountSid,
|
||||
r[0], this.webhook_secret);
|
||||
this.queueEventHookRequestor = new Requestor(this.logger, r[0]);
|
||||
this.queueEventHook = r[0];
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1060,7 +797,7 @@ class CallSession extends Emitter {
|
||||
/* send webhook */
|
||||
const params = {...obj, ...this.callInfo.toJSON()};
|
||||
this.logger.info({accountSid: this.accountSid, params}, 'performQueueWebhook: sending webhook');
|
||||
this.queueEventHookRequestor.request('queue:status', this.queueEventHook, params)
|
||||
this.queueEventHookRequestor.request(this.queueEventHook, params)
|
||||
.catch((err) => {
|
||||
this.logger.info({err, accountSid: this.accountSid, obj}, 'Error sending queue notification event');
|
||||
});
|
||||
@@ -1149,43 +886,10 @@ class CallSession extends Emitter {
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('CallSession: call terminated by jambones');
|
||||
origDestroy();
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async releaseMediaToSBC(remoteSdp) {
|
||||
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
|
||||
await this.dlg.modify(remoteSdp, {
|
||||
headers: {
|
||||
'X-Reason': 'release-media'
|
||||
}
|
||||
});
|
||||
this.ep.destroy()
|
||||
.then(() => this.ep = null)
|
||||
.catch((err) => this.logger.error({err}, 'CallSession:releaseMediaToSBC: Error destroying endpoint'));
|
||||
}
|
||||
|
||||
async reAnchorMedia() {
|
||||
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
await this.dlg.modify(this.ep.local.sdp, {
|
||||
headers: {
|
||||
'X-Reason': 'anchor-media'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1195,49 +899,26 @@ class CallSession extends Emitter {
|
||||
* @param {number} sipStatus - current sip status
|
||||
* @param {number} [duration] - duration of a completed call, in seconds
|
||||
*/
|
||||
_notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
_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');
|
||||
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON());
|
||||
this.notifier.request(this.call_status_hook, this.callInfo);
|
||||
} catch (err) {
|
||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
// update calls db
|
||||
//this.logger.debug(`updating redis with ${JSON.stringify(this.callInfo)}`);
|
||||
this.updateCallStatus(Object.assign({}, this.callInfo.toJSON()), this.serviceUrl)
|
||||
this.updateCallStatus(Object.assign({}, this.callInfo), this.serviceUrl)
|
||||
.catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
|
||||
_awaitCommandsOrHangup() {
|
||||
assert(!this.wakeupResolver);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.logger.info('_awaitCommandsOrHangup - waiting...');
|
||||
this.wakeupResolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_clearTasks(evt) {
|
||||
if (this.requestor instanceof WsRequestor) {
|
||||
this.logger.info({evt}, 'CallSession:_clearTasks on event from background gather');
|
||||
try {
|
||||
this.kill();
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallSession;
|
||||
|
||||
@@ -8,17 +8,14 @@ const CallSession = require('./call-session');
|
||||
|
||||
*/
|
||||
class ConfirmCallSession extends CallSession {
|
||||
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName}) {
|
||||
constructor({logger, application, dlg, ep, tasks, callInfo}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
srf: dlg.srf,
|
||||
callSid: dlg.callSid,
|
||||
tasks,
|
||||
callInfo,
|
||||
accountInfo,
|
||||
memberId,
|
||||
confName
|
||||
callInfo
|
||||
});
|
||||
this.dlg = dlg;
|
||||
this.ep = ep;
|
||||
@@ -30,10 +27,6 @@ class ConfirmCallSession extends CallSession {
|
||||
_clearResources() {
|
||||
}
|
||||
|
||||
_callerHungup() {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = ConfirmCallSession;
|
||||
|
||||
@@ -21,23 +21,13 @@ class InboundCallSession extends CallSession {
|
||||
this.req = req;
|
||||
this.res = res;
|
||||
|
||||
req.once('cancel', this._onCancel.bind(this));
|
||||
req.on('cancel', () => {
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
|
||||
this._callReleased();
|
||||
});
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.Trying,
|
||||
sipStatus: 100,
|
||||
sipReason: 'Trying'
|
||||
});
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.NoAnswer,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Request Terminated'
|
||||
});
|
||||
this._callReleased();
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
}
|
||||
|
||||
_onTasksDone() {
|
||||
@@ -55,7 +45,6 @@ class InboundCallSession extends CallSession {
|
||||
this.res.send(603);
|
||||
}
|
||||
}
|
||||
this.req.removeAllListeners('cancel');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,13 +53,9 @@ class InboundCallSession extends CallSession {
|
||||
_callerHungup() {
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Completed,
|
||||
duration
|
||||
});
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('InboundCallSession: caller hung up');
|
||||
this._callReleased();
|
||||
this.req.removeAllListeners('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,7 @@ class RestCallSession extends CallSession {
|
||||
this.ep = ep;
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.Trying,
|
||||
sipStatus: 100,
|
||||
sipReason: 'Trying'
|
||||
});
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,6 @@ const WAIT = 'wait';
|
||||
const JOIN = 'join';
|
||||
const START = 'start';
|
||||
|
||||
|
||||
function confNoMatch(str) {
|
||||
return str.match(/^No active conferences/) || str.match(/Conference.*not found/);
|
||||
}
|
||||
@@ -28,8 +27,7 @@ function camelize(str) {
|
||||
|
||||
function unhandled(logger, cs, evt) {
|
||||
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
||||
// logger.debug({evt}, `unhandled conference event: ${evt.getHeader('Action')}`) ;
|
||||
logger.debug(`unhandled conference event: ${evt.getHeader('Action')}`) ;
|
||||
logger.debug({evt}, `unhandled conference event: ${evt.getHeader('Action')}`) ;
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
@@ -47,10 +45,10 @@ class Conference extends Task {
|
||||
|
||||
this.confName = this.data.name;
|
||||
[
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit',
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
|
||||
].forEach((attr) => this[attr] = this.data[attr]);
|
||||
this.record = this.data.record || {};
|
||||
|
||||
this.statusEvents = [];
|
||||
if (this.statusHook) {
|
||||
['start', 'end', 'join', 'leave', 'start-talking', 'stop-talking'].forEach((e) => {
|
||||
@@ -69,9 +67,6 @@ class Conference extends Task {
|
||||
|
||||
get name() { return TaskName.Conference; }
|
||||
|
||||
get shouldRecord() { return this.record.path; }
|
||||
get isRecording() { return this.recordingInProgress; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
@@ -218,7 +213,6 @@ class Conference extends Task {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
cs.clearConferenceDetails();
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -336,30 +330,15 @@ class Conference extends Task {
|
||||
|
||||
const opts = {};
|
||||
if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
|
||||
if (this.startConferenceOnEnter) Object.assign(opts, {flags: {moderator: true}});
|
||||
if (this.joinMuted) Object.assign(opts, {flags: {mute: true}});
|
||||
|
||||
try {
|
||||
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
|
||||
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
|
||||
this.memberId = memberId;
|
||||
this.confUuid = confUuid;
|
||||
|
||||
cs.setConferenceDetails(memberId, this.confName, confUuid);
|
||||
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
|
||||
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
|
||||
this._notifyConferenceEvent(cs, 'join');
|
||||
|
||||
// start recording if requested and we just started the conference
|
||||
if (startConf && this.shouldRecord) {
|
||||
this.logger.info(`recording conference to ${this.record.path}`);
|
||||
try {
|
||||
await this.ep.api(`conference ${this.confName} record ${this.record.path}`);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Conference:_joinConference - failed to start recording');
|
||||
}
|
||||
}
|
||||
|
||||
// listen for conference events
|
||||
this.ep.filter('Conference-Unique-ID', this.confUuid);
|
||||
this.ep.conn.on('esl::event::CUSTOM::*', this.__onConferenceEvent.bind(this, cs)) ;
|
||||
@@ -392,70 +371,9 @@ class Conference extends Task {
|
||||
*/
|
||||
notifyStartConference(cs, opts) {
|
||||
this.logger.info({opts}, `Conference:notifyStartConference: conference ${this.confName} has now started`);
|
||||
this.conferenceStartTime = new Date();
|
||||
this.emitter.emit('join', opts);
|
||||
}
|
||||
|
||||
async doConferenceMuteNonModerators(cs, opts) {
|
||||
const mute = opts.conf_mute_status === 'mute';
|
||||
assert (cs.isInConference);
|
||||
|
||||
this.logger.info(`Conference:doConferenceMuteNonModerators ${mute ? 'muting' : 'unmuting'} non-moderators`);
|
||||
this.ep.api(`conference ${this.confName} ${mute ? 'mute' : 'unmute'} non_moderator`)
|
||||
.catch((err) => this.logger.info({err}, 'Error muting or unmuting non_moderators'));
|
||||
|
||||
if (this.conf_hold_status !== 'hold' && this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
async doConferenceHold(cs, opts) {
|
||||
assert (cs.isInConference);
|
||||
|
||||
const {conf_hold_status, wait_hook} = opts;
|
||||
let hookOnly = true;
|
||||
|
||||
if (this.conf_hold_status !== conf_hold_status) {
|
||||
hookOnly = false;
|
||||
this.conf_hold_status = conf_hold_status;
|
||||
const hold = conf_hold_status === 'hold';
|
||||
|
||||
this.ep.api(`conference ${this.confName} ${hold ? 'mute' : 'unmute'} ${this.memberId}`)
|
||||
.catch((err) => this.logger.info({err}, 'Error muting or unmuting participant'));
|
||||
this.ep.api(`conference ${this.confName} ${hold ? 'deaf' : 'undeaf'} ${this.memberId}`)
|
||||
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
|
||||
}
|
||||
|
||||
if (hookOnly && this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
if (wait_hook && this.conf_hold_status === 'hold') {
|
||||
const {dlg} = cs;
|
||||
this._doWaitHookWhileOnHold(cs, dlg, wait_hook);
|
||||
}
|
||||
else if (this.conf_hold_status !== 'hold' && this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
|
||||
do {
|
||||
try {
|
||||
const tasks = await this._playHook(cs, dlg, wait_hook);
|
||||
if (0 === tasks.length) break;
|
||||
} catch (err) {
|
||||
if (!this.killed) {
|
||||
this.logger.info(err, `Conference:_doWait: failed retrieving wait_hook for ${this.confName}`);
|
||||
}
|
||||
this._playSession = null;
|
||||
break;
|
||||
}
|
||||
} while (!this.killed && this.conf_hold_status !== 'hold');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ourselves to the waitlist of sessions to be notified once
|
||||
* the conference starts
|
||||
@@ -529,7 +447,7 @@ class Conference extends Task {
|
||||
|
||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
|
||||
assert(!this._playSession);
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, cs.callInfo);
|
||||
const json = await cs.application.requestor.request(hook, cs.callInfo);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
@@ -546,9 +464,6 @@ class Conference extends Task {
|
||||
dlg,
|
||||
ep: cs.ep,
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo,
|
||||
memberId: this.memberId,
|
||||
confName: this.confName,
|
||||
tasks
|
||||
});
|
||||
await this._playSession.exec();
|
||||
@@ -569,7 +484,6 @@ class Conference extends Task {
|
||||
}
|
||||
|
||||
async replaceEndpointAndEnd(cs) {
|
||||
cs.clearConferenceDetails();
|
||||
if (this.replaced) return;
|
||||
this.replaced = true;
|
||||
try {
|
||||
@@ -586,7 +500,7 @@ class Conference extends Task {
|
||||
params.duration = (Date.now() - this.conferenceStartTime.getTime()) / 1000;
|
||||
if (!params.time) params.time = (new Date()).toISOString();
|
||||
if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount;
|
||||
cs.application.requestor.request('verb:hook', this.statusHook, Object.assign(params, this.statusParams))
|
||||
cs.application.requestor.request(this.statusHook, Object.assign(params, this.statusParams))
|
||||
.catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
|
||||
class TaskConfig extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'synthesizer',
|
||||
'recognizer',
|
||||
'bargeIn'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if (this.hasBargeIn && this.bargeIn.enable === true) {
|
||||
this.gatherOpts = {
|
||||
verb: 'gather',
|
||||
timeout: 0,
|
||||
bargein: true
|
||||
};
|
||||
[
|
||||
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'bargein', 'dtmfBargein', 'minBargeinWordCount', 'actionHook'
|
||||
].forEach((k) => {
|
||||
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
|
||||
});
|
||||
}
|
||||
this.preconditions = this.hasBargeIn ? TaskPreconditions.Endpoint : TaskPreconditions.None;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Config; }
|
||||
|
||||
get hasSynthesizer() { return Object.keys(this.synthesizer).length; }
|
||||
|
||||
get hasRecognizer() { return Object.keys(this.recognizer).length; }
|
||||
|
||||
get hasBargeIn() { return Object.keys(this.bargeIn).length; }
|
||||
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
|
||||
if (this.hasSynthesizer) {
|
||||
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
|
||||
? this.synthesizer.vendor
|
||||
: cs.speechSynthesisVendor;
|
||||
cs.speechSynthesisLanguage = this.synthesizer.language !== 'default'
|
||||
? this.synthesizer.language
|
||||
: cs.speechSynthesisLanguage;
|
||||
cs.speechSynthesisVoice = this.synthesizer.voice !== 'default'
|
||||
? this.synthesizer.voice
|
||||
: cs.speechSynthesisVoice;
|
||||
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
|
||||
}
|
||||
if (this.hasRecognizer) {
|
||||
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
|
||||
? this.recognizer.vendor
|
||||
: cs.speechRecognizerVendor;
|
||||
cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
|
||||
? this.recognizer.language
|
||||
: cs.speechRecognizerLanguage;
|
||||
this.logger.info({recognizer: this.recognizer}, 'Config: updated recognizer');
|
||||
}
|
||||
if (this.hasBargeIn) {
|
||||
if (this.gatherOpts) {
|
||||
this.logger.debug({opts: this.gatherOpts}, 'Config: enabling bargeIn');
|
||||
cs.enableBotMode(this.gatherOpts);
|
||||
}
|
||||
else {
|
||||
this.logger.debug('Config: disabling bargeIn');
|
||||
cs.disableBotMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskConfig;
|
||||
@@ -12,9 +12,7 @@ const assert = require('assert');
|
||||
const placeCall = require('../utils/place-outdial');
|
||||
const sessionTracker = require('../session/session-tracker');
|
||||
const DtmfCollector = require('../utils/dtmf-collector');
|
||||
const dbUtils = require('../utils/db-utils');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
|
||||
function parseDtmfOptions(logger, dtmfCapture) {
|
||||
let parentDtmfCollector, childDtmfCollector;
|
||||
@@ -92,7 +90,6 @@ class TaskDial extends Task {
|
||||
this.timeLimit = this.data.timeLimit;
|
||||
this.confirmHook = this.data.confirmHook;
|
||||
this.confirmMethod = this.data.confirmMethod;
|
||||
this.referHook = this.data.referHook;
|
||||
this.dtmfHook = this.data.dtmfHook;
|
||||
this.proxy = this.data.proxy;
|
||||
|
||||
@@ -133,15 +130,6 @@ class TaskDial extends Task {
|
||||
|
||||
get name() { return TaskName.Dial; }
|
||||
|
||||
get canReleaseMedia() {
|
||||
return !process.env.ANCHOR_MEDIA_ALWAYS && !this.listenTask && !this.transcribeTask;
|
||||
}
|
||||
|
||||
get summary() {
|
||||
if (this.target.length === 1) return `${this.name}{type=${this.target[0].type}}`;
|
||||
else return `${this.name}{${this.target.length} targets}`;
|
||||
}
|
||||
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
try {
|
||||
@@ -154,12 +142,13 @@ class TaskDial extends Task {
|
||||
this.epOther.play(this.dialMusic).catch((err) => {});
|
||||
}
|
||||
}
|
||||
if (!this.killed) await this._attemptCalls(cs);
|
||||
if (this.epOther) this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
|
||||
await this._attemptCalls(cs);
|
||||
await this.awaitTaskDone();
|
||||
this.logger.debug({callSid: this.cs.callSid}, 'Dial:exec task is done, sending actionHook if any');
|
||||
await this.performAction(this.results, this.killReason !== KillReason.Replaced);
|
||||
this._removeDtmfDetection(cs.dlg);
|
||||
this._removeDtmfDetection(this.dlg);
|
||||
this._removeDtmfDetection(cs, this.epOther);
|
||||
this._removeDtmfDetection(cs, this.ep);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskDial:exec terminating with error');
|
||||
this.kill(cs);
|
||||
@@ -168,30 +157,18 @@ 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._removeDtmfDetection(this.cs, this.epOther);
|
||||
this._removeDtmfDetection(this.cs, this.ep);
|
||||
this._killOutdials();
|
||||
if (this.sd) {
|
||||
this.sd.kill();
|
||||
this.sd.removeAllListeners();
|
||||
this.sd = null;
|
||||
}
|
||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||
if (this.listenTask) await this.listenTask.kill(cs);
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -200,14 +177,9 @@ class TaskDial extends Task {
|
||||
* @param {*} tasks - array of play/say tasks to execute
|
||||
*/
|
||||
async whisper(tasks, callSid) {
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
|
||||
try {
|
||||
const cs = this.callSession;
|
||||
if (!this.ep && !this.epOther) {
|
||||
await this.reAnchorMedia(this.callSession, this.sd);
|
||||
}
|
||||
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
|
||||
|
||||
this.logger.debug('Dial:whisper unbridging endpoints');
|
||||
await this.epOther.unbridge();
|
||||
this.logger.debug('Dial:whisper executing tasks');
|
||||
@@ -216,12 +188,7 @@ class TaskDial extends Task {
|
||||
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
|
||||
}
|
||||
this.logger.debug('Dial:whisper tasks complete');
|
||||
if (!cs.callGone && this.epOther) {
|
||||
|
||||
/* if we can release the media back to the SBC, do so now */
|
||||
if (this.canReleaseMedia) this._releaseMedia(cs, this.sd);
|
||||
else this.epOther.bridge(this.ep);
|
||||
}
|
||||
if (!cs.callGone && this.epOther) this.epOther.bridge(this.ep);
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Dial:whisper error');
|
||||
}
|
||||
@@ -231,106 +198,54 @@ class TaskDial extends Task {
|
||||
* mute or unmute one side of the call
|
||||
*/
|
||||
async mute(callSid, doMute) {
|
||||
const parentCall = callSid !== this.callSid;
|
||||
const dlg = parentCall ? this.callSession.dlg : this.dlg;
|
||||
const hdr = `${doMute ? 'mute' : 'unmute'} call leg`;
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:mute: no paired endpoint found');
|
||||
try {
|
||||
/* let rtpengine do the mute / unmute */
|
||||
await dlg.request({
|
||||
method: 'INFO',
|
||||
headers: {
|
||||
'X-Reason': hdr
|
||||
}
|
||||
});
|
||||
const parentCall = callSid !== this.callSid;
|
||||
const ep = parentCall ? this.epOther : this.ep;
|
||||
await ep[doMute ? 'mute' : 'unmute']();
|
||||
this.logger.debug(`Dial:mute ${doMute ? 'muted' : 'unmuted'} ${parentCall ? 'parentCall' : 'childCall'}`);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, `Dial:mute - ${hdr} error`);
|
||||
this.logger.error(err, 'Dial:mute error');
|
||||
}
|
||||
}
|
||||
|
||||
async handleRefer(cs, req, res, callInfo = cs.callInfo) {
|
||||
if (this.referHook) {
|
||||
try {
|
||||
const isChild = !!callInfo.parentCallSid;
|
||||
const referring_call_sid = isChild ? callInfo.callSid : cs.callSid;
|
||||
const referred_call_sid = isChild ? callInfo.parentCallSid : this.sd.callSid;
|
||||
|
||||
const to = parseUri(req.getParsedHeader('Refer-To').uri);
|
||||
const by = parseUri(req.getParsedHeader('Referred-By').uri);
|
||||
this.logger.info({to}, 'refer to parsed');
|
||||
await cs.requestor.request('verb:hook', this.referHook, {
|
||||
...callInfo,
|
||||
refer_details: {
|
||||
sip_refer_to: req.get('Refer-To'),
|
||||
sip_referred_by: req.get('Referred-By'),
|
||||
sip_user_agent: req.get('User-Agent'),
|
||||
refer_to_user: to.user,
|
||||
referred_by_user: by.user,
|
||||
referring_call_sid,
|
||||
referred_call_sid
|
||||
}
|
||||
});
|
||||
res.send(202);
|
||||
this.logger.info('DialTask:handleRefer - sent 202 Accepted');
|
||||
} catch (err) {
|
||||
res.send(err.statusCode || 501);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.logger.info('DialTask:handleRefer - got REFER but no referHook, responding 501');
|
||||
res.send(501);
|
||||
}
|
||||
}
|
||||
|
||||
_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();
|
||||
}
|
||||
|
||||
_installDtmfDetection(cs, dlg) {
|
||||
dlg.on('info', this._onInfo.bind(this, cs, dlg));
|
||||
_installDtmfDetection(cs, ep, dtmfDetector) {
|
||||
if (ep && this.dtmfHook && !ep.dtmfDetector) {
|
||||
ep.dtmfDetector = dtmfDetector;
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
}
|
||||
_removeDtmfDetection(dlg) {
|
||||
dlg && dlg.removeAllListeners('info');
|
||||
_removeDtmfDetection(cs, ep) {
|
||||
if (ep) {
|
||||
delete ep.dtmfDetector;
|
||||
ep.removeAllListeners('dtmf');
|
||||
}
|
||||
}
|
||||
|
||||
_onInfo(cs, dlg, req, res) {
|
||||
res.send(200);
|
||||
if (req.get('Content-Type') !== 'application/dtmf-relay') return;
|
||||
|
||||
const dtmfDetector = dlg === cs.dlg ? this.parentDtmfCollector : this.childDtmfCollector;
|
||||
if (!dtmfDetector) return;
|
||||
let requestor, callSid, callInfo;
|
||||
if (dtmfDetector === this.parentDtmfCollector) {
|
||||
requestor = cs.requestor;
|
||||
callSid = cs.callSid;
|
||||
callInfo = cs.callInfo;
|
||||
}
|
||||
else {
|
||||
requestor = this.sd?.requestor;
|
||||
callSid = this.sd?.callSid;
|
||||
callInfo = this.sd?.callInfo;
|
||||
}
|
||||
if (!requestor) return;
|
||||
const arr = /Signal=([0-9#*])/.exec(req.body);
|
||||
if (!arr) return;
|
||||
const key = arr[1];
|
||||
const match = dtmfDetector.keyPress(key);
|
||||
if (match) {
|
||||
this.logger.info({callSid}, `Dial:_onInfo triggered dtmf match: ${match}`);
|
||||
requestor.request('verb:hook', this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
_onDtmf(cs, ep, evt) {
|
||||
if (ep.dtmfDetector) {
|
||||
const match = ep.dtmfDetector.keyPress(evt.dtmf);
|
||||
if (match) {
|
||||
this.logger.debug({callSid: this.cs.callSid}, `Dial:_onDtmf triggered dtmf match: ${match}`);
|
||||
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
|
||||
cs.requestor :
|
||||
(this.sd ? this.sd.requestor : null);
|
||||
if (!requestor) {
|
||||
this.logger.info(`Dial:_onDtmf got digits on B leg after adulting: ${evt.dtmf}`);
|
||||
}
|
||||
else {
|
||||
requestor.request(this.dtmfHook, {dtmf: match, ...cs.callInfo})
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +255,7 @@ class TaskDial extends Task {
|
||||
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
|
||||
|
||||
/* send outbound legs back to the same SBC (to support static IP feature) */
|
||||
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port}`;
|
||||
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port};transport=tcp`;
|
||||
|
||||
if (this.dialMusic) {
|
||||
// play dial music to caller while we outdial
|
||||
@@ -354,7 +269,6 @@ class TaskDial extends Task {
|
||||
const {req, srf} = cs;
|
||||
const {getSBC} = srf.locals;
|
||||
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
|
||||
const {lookupCarrier} = dbUtils(this.logger, cs.srf);
|
||||
const sbcAddress = this.proxy || getSBC();
|
||||
const teamsInfo = {};
|
||||
let fqdn;
|
||||
@@ -378,9 +292,8 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
const ms = await cs.getMS();
|
||||
this.timerRing = setTimeout(() => {
|
||||
const timerRing = setTimeout(() => {
|
||||
this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`);
|
||||
this.timerRing = null;
|
||||
this._killOutdials();
|
||||
}, this.timeout * 1000);
|
||||
|
||||
@@ -401,16 +314,6 @@ class TaskDial extends Task {
|
||||
this.logger.error({err}, 'Error looking up account by sid');
|
||||
}
|
||||
}
|
||||
if (t.type === 'phone' && t.trunk) {
|
||||
const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk);
|
||||
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk})`);
|
||||
if (voip_carrier_sid) {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.killed) return;
|
||||
|
||||
const sd = placeCall({
|
||||
logger: this.logger,
|
||||
application: cs.application,
|
||||
@@ -419,17 +322,13 @@ class TaskDial extends Task {
|
||||
sbcAddress,
|
||||
target: t,
|
||||
opts,
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo
|
||||
callInfo: cs.callInfo
|
||||
});
|
||||
this.dials.set(sd.callSid, sd);
|
||||
|
||||
sd
|
||||
.on('refer', (callInfo, req, res) => this.handleRefer(cs, req, res, callInfo))
|
||||
.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);
|
||||
@@ -453,8 +352,7 @@ class TaskDial extends Task {
|
||||
break;
|
||||
case CallStatus.InProgress:
|
||||
this.logger.debug('Dial:_attemptCall -- call was answered');
|
||||
clearTimeout(this.timerRing);
|
||||
this.timerRing = null;
|
||||
clearTimeout(timerRing);
|
||||
break;
|
||||
case CallStatus.Failed:
|
||||
case CallStatus.Busy:
|
||||
@@ -462,41 +360,23 @@ 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(this.timerRing);
|
||||
this.timerRing = null;
|
||||
clearTimeout(timerRing);
|
||||
this.kill(cs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.on('accept', async() => {
|
||||
.on('accept', () => {
|
||||
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
||||
clearTimeout(this.timerRing);
|
||||
try {
|
||||
await this._connectSingleDial(cs, sd);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial ');
|
||||
}
|
||||
this._connectSingleDial(cs, sd);
|
||||
})
|
||||
.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');
|
||||
}
|
||||
})
|
||||
.on('refer', (callInfo, req, res) => {
|
||||
|
||||
})
|
||||
.once('adulting', () => {
|
||||
/* child call just adulted and got its own session */
|
||||
@@ -514,8 +394,8 @@ class TaskDial extends Task {
|
||||
});
|
||||
}
|
||||
|
||||
async _connectSingleDial(cs, sd) {
|
||||
if (!this.bridged && !this.canReleaseMedia) {
|
||||
_connectSingleDial(cs, sd) {
|
||||
if (!this.bridged) {
|
||||
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
|
||||
if (this.epOther) {
|
||||
this.epOther.api('uuid_break', this.epOther.uuid);
|
||||
@@ -525,16 +405,10 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
// ding! ding! ding! we have a winner
|
||||
await this._selectSingleDial(cs, sd);
|
||||
this._selectSingleDial(cs, sd);
|
||||
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
|
||||
@@ -544,7 +418,7 @@ class TaskDial extends Task {
|
||||
* - launch any nested tasks
|
||||
* - and establish a handler to clean up if the called party hangs up
|
||||
*/
|
||||
async _selectSingleDial(cs, sd) {
|
||||
_selectSingleDial(cs, sd) {
|
||||
debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`);
|
||||
this.dials.delete(sd.callSid);
|
||||
|
||||
@@ -552,10 +426,14 @@ class TaskDial extends Task {
|
||||
this.callSid = sd.callSid;
|
||||
if (this.earlyMedia) {
|
||||
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
||||
await cs.propagateAnswer();
|
||||
cs.propagateAnswer();
|
||||
}
|
||||
if (this.timeLimit) {
|
||||
this.timerMaxCallDuration = setTimeout(this._onMaxCallDuration.bind(this, cs), this.timeLimit * 1000);
|
||||
this.timerMaxCallDuration = setTimeout(() => {
|
||||
this.logger.info(`Dial:_selectSingleDial tearing down call as it has reached ${this.timeLimit}s`);
|
||||
this.ep.unbridge();
|
||||
this.kill(cs);
|
||||
}, this.timeLimit * 1000);
|
||||
}
|
||||
sessionTracker.add(this.callSid, cs);
|
||||
this.dlg.on('destroy', () => {
|
||||
@@ -563,11 +441,8 @@ class TaskDial extends Task {
|
||||
if (this.dlg) {
|
||||
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
|
||||
sessionTracker.remove(this.callSid);
|
||||
if (this.timerMaxCallDuration) {
|
||||
clearTimeout(this.timerMaxCallDuration);
|
||||
this.timerMaxCallDuration = null;
|
||||
}
|
||||
this.ep && this.ep.unbridge();
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.ep.unbridge();
|
||||
this.kill(cs);
|
||||
}
|
||||
});
|
||||
@@ -578,14 +453,10 @@ class TaskDial extends Task {
|
||||
dialCallSid: sd.callSid,
|
||||
});
|
||||
|
||||
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.ep, this.childDtmfCollector);
|
||||
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.ep);
|
||||
|
||||
/* if we can release the media back to the SBC, do so now */
|
||||
if (this.canReleaseMedia) this._releaseMedia(cs, sd);
|
||||
}
|
||||
|
||||
_bridgeEarlyMedia(sd) {
|
||||
@@ -597,38 +468,6 @@ class TaskDial extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the media from freeswitch
|
||||
* @param {*} cs
|
||||
* @param {*} sd
|
||||
*/
|
||||
async _releaseMedia(cs, sd) {
|
||||
assert(cs.ep && sd.ep);
|
||||
|
||||
try {
|
||||
const aLegSdp = cs.ep.remote.sdp;
|
||||
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) {
|
||||
this.logger.info({err}, 'Dial:_releaseMedia error');
|
||||
}
|
||||
}
|
||||
|
||||
async reAnchorMedia(cs, sd) {
|
||||
if (cs.ep && sd.ep) return;
|
||||
|
||||
this.logger.info('Dial:reAnchorMedia - re-anchoring media to freewitch');
|
||||
await Promise.all([sd.reAnchorMedia(), cs.reAnchorMedia()]);
|
||||
this.epOther = cs.ep;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -9,22 +9,10 @@ class Dialogflow extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.credentials = this.data.credentials;
|
||||
|
||||
/* set project id with environment and region (optionally) */
|
||||
if (this.data.environment && this.data.region) {
|
||||
this.project = `${this.data.project}:${this.data.environment}:${this.data.region}`;
|
||||
}
|
||||
else if (this.data.environment) {
|
||||
this.project = `${this.data.project}:${this.data.environment}`;
|
||||
}
|
||||
else if (this.data.region) {
|
||||
this.project = `${this.data.project}::${this.data.region}`;
|
||||
}
|
||||
else {
|
||||
this.project = this.data.project;
|
||||
}
|
||||
|
||||
if (this.data.environment) this.project = `${this.data.project}:${this.data.environment}`;
|
||||
else this.project = this.data.project;
|
||||
this.lang = this.data.lang || 'en-US';
|
||||
this.welcomeEvent = this.data.welcomeEvent || '';
|
||||
if (this.welcomeEvent.length && this.data.welcomeEventParams && typeof this.data.welcomeEventParams === 'object') {
|
||||
@@ -210,7 +198,6 @@ class Dialogflow extends Task {
|
||||
/* if we are using tts and a message was provided, play it out */
|
||||
if (this.vendor && intent.fulfillmentText && intent.fulfillmentText.length > 0) {
|
||||
const {srf} = cs;
|
||||
const {stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
this.waitingForPlayStart = false;
|
||||
|
||||
@@ -230,7 +217,7 @@ class Dialogflow extends Task {
|
||||
credentials: this.ttsCredentials
|
||||
};
|
||||
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, obj);
|
||||
const {filePath, servedFromCache} = await synthAudio(obj);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
|
||||
|
||||
@@ -452,8 +439,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('verb:hook', hook, {...results, ...cs.callInfo.toJSON()});
|
||||
async _performHook(cs, hook, results) {
|
||||
const json = await this.cs.requestor.request(hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('../make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -79,14 +79,12 @@ class TaskEnqueue extends Task {
|
||||
this.notifyUrl = url;
|
||||
|
||||
/* invoke account-level webhook for queue event notifications */
|
||||
try {
|
||||
cs.performQueueWebhook({
|
||||
event: 'join',
|
||||
queue: this.data.name,
|
||||
length: members,
|
||||
joinTime: this.waitStartTime
|
||||
});
|
||||
} catch (err) {}
|
||||
cs.performQueueWebhook({
|
||||
event: 'join',
|
||||
queue: this.data.name,
|
||||
length: members,
|
||||
joinTime: this.waitStartTime
|
||||
});
|
||||
}
|
||||
|
||||
async _removeFromQueue(cs) {
|
||||
@@ -116,7 +114,6 @@ class TaskEnqueue extends Task {
|
||||
this.bridgeDetails = opts;
|
||||
this.logger.info({bridgeDetails: this.bridgeDetails}, `time to dequeue from ${this.queueName}`);
|
||||
if (this._playSession) {
|
||||
this._leave = false;
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
@@ -236,7 +233,6 @@ class TaskEnqueue extends Task {
|
||||
});
|
||||
|
||||
// resolve when either side hangs up
|
||||
this.state = QueueResults.Bridged;
|
||||
this.emitter
|
||||
.on('hangup', () => {
|
||||
this.logger.info('TaskEnqueue:_bridgeLocal ending with hangup from dequeue party');
|
||||
@@ -317,7 +313,7 @@ class TaskEnqueue extends Task {
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
|
||||
}
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, params);
|
||||
const json = await cs.application.requestor.request(hook, params);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
@@ -329,15 +325,16 @@ class TaskEnqueue extends Task {
|
||||
|
||||
// check for 'leave' verb and only execute tasks up till then
|
||||
const tasksToRun = [];
|
||||
let leave = false;
|
||||
for (const o of tasks) {
|
||||
if (o.name === TaskName.Leave) {
|
||||
this._leave = true;
|
||||
leave = true;
|
||||
this.logger.info('waitHook returned a leave task');
|
||||
break;
|
||||
}
|
||||
tasksToRun.push(o);
|
||||
}
|
||||
const cloneTasks = [...tasksToRun];
|
||||
|
||||
if (this.killed) return [];
|
||||
else if (tasksToRun.length > 0) {
|
||||
this._playSession = new ConfirmCallSession({
|
||||
@@ -346,17 +343,16 @@ class TaskEnqueue extends Task {
|
||||
dlg,
|
||||
ep: cs.ep,
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo,
|
||||
tasks: tasksToRun
|
||||
});
|
||||
await this._playSession.exec();
|
||||
this._playSession = null;
|
||||
}
|
||||
if (this._leave) {
|
||||
if (leave) {
|
||||
this.state = QueueResults.Leave;
|
||||
this.kill(cs);
|
||||
}
|
||||
return cloneTasks;
|
||||
return tasksToRun;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,57 +3,36 @@ const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents
|
||||
AwsTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
|
||||
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) {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
'finishOnKey', 'hints', 'input', 'numDigits',
|
||||
'partialResultHook',
|
||||
'speechTimeout', 'timeout', 'say', 'play'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
|
||||
/* when collecting dtmf, bargein on dtmf is true unless explicitly set to false */
|
||||
if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true;
|
||||
|
||||
/* timeout of zero means no timeout */
|
||||
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
|
||||
this.interim = this.partialResultHook || this.bargein;
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 0;
|
||||
this.timeout = (this.timeout || 5) * 1000;
|
||||
this.interim = this.partialResultCallback;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
this.hints = recognizer.hints || [];
|
||||
this.hintsBoost = recognizer.hintsBoost;
|
||||
this.altLanguages = recognizer.altLanguages || [];
|
||||
this.punctuation = !!recognizer.punctuation;
|
||||
|
||||
/* 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};
|
||||
|
||||
/* aws options */
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
this.filterMethod = recognizer.filterMethod;
|
||||
|
||||
/* microsoft options */
|
||||
this.outputFormat = recognizer.outputFormat || 'simple';
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
|
||||
}
|
||||
|
||||
this.digitBuffer = '';
|
||||
@@ -61,37 +40,16 @@ 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.listenDuringPrompt = false;
|
||||
|
||||
this.parentTask = parentTask;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Gather; }
|
||||
|
||||
get needsStt() { return this.input.includes('speech'); }
|
||||
|
||||
get earlyMedia() {
|
||||
return (this.sayTask && this.sayTask.earlyMedia) ||
|
||||
(this.playTask && this.playTask.earlyMedia);
|
||||
}
|
||||
|
||||
get summary() {
|
||||
let s = `${this.name}{`;
|
||||
if (this.input.length === 2) s += 'inputs=[speech,digits],';
|
||||
else if (this.input.includes('digits')) s += 'inputs=digits';
|
||||
else s += 'inputs=speech,';
|
||||
|
||||
if (this.input.includes('speech')) {
|
||||
s += `vendor=${this.vendor || 'default'},language=${this.language || 'default'}`;
|
||||
}
|
||||
if (this.sayTask) s += ',with nested say task';
|
||||
if (this.playTask) s += ',with nested play task';
|
||||
s += '}';
|
||||
return s;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
this.logger.debug('Gather:exec');
|
||||
await super.exec(cs);
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
|
||||
@@ -99,9 +57,9 @@ class TaskGather extends Task {
|
||||
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
|
||||
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
if (this.needsStt && !this.sttCredentials) {
|
||||
if (!this.sttCredentials) {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
|
||||
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but not creds supplied`);
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
@@ -111,44 +69,30 @@ 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 (err) return this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.debug('Gather: say task completed');
|
||||
if (!this.killed) this._startTimer();
|
||||
});
|
||||
}
|
||||
else if (this.playTask) {
|
||||
this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.on('playDone', (err) => {
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
if (!this.killed) startListening(cs, ep);
|
||||
if (!this.killed) this._startTimer();
|
||||
});
|
||||
}
|
||||
else startListening(cs, ep);
|
||||
else this._startTimer();
|
||||
|
||||
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
||||
if (this.input.includes('speech')) {
|
||||
await this._initSpeech(cs, ep);
|
||||
this._startTranscribing(ep);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
}
|
||||
|
||||
if (this.input.includes('digits') || this.dtmfBargein) {
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
if (this.input.includes('digits')) {
|
||||
ep.on('dtmf', this._onDtmf.bind(this, ep));
|
||||
}
|
||||
|
||||
await this.awaitTaskDone();
|
||||
@@ -157,77 +101,38 @@ class TaskGather extends Task {
|
||||
}
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.VadDetected);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.VadDetected);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.VadDetected);
|
||||
}
|
||||
|
||||
kill(cs) {
|
||||
super.kill(cs);
|
||||
this._killAudio(cs);
|
||||
this._killAudio();
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
this._resolve('killed');
|
||||
}
|
||||
|
||||
updateTimeout(timeout) {
|
||||
this.logger.info(`TaskGather:updateTimout - updating timeout to ${timeout}`);
|
||||
this.timeout = timeout;
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
_onDtmf(ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
let resolved = false;
|
||||
if (this.dtmfBargein) this._killAudio(cs);
|
||||
if (evt.dtmf === this.finishOnKey && this.input.includes('digits')) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-terminator-key');
|
||||
}
|
||||
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
||||
else {
|
||||
this.digitBuffer += evt.dtmf;
|
||||
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);
|
||||
if (this.digitBuffer.length === this.numDigits) this._resolve('dtmf-num-digits');
|
||||
}
|
||||
this._killAudio();
|
||||
}
|
||||
|
||||
async _initSpeech(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;
|
||||
}
|
||||
|
||||
if ('google' === this.vendor) {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
Object.assign(opts, {
|
||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
||||
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
|
||||
GOOGLE_SPEECH_MODEL: 'command_and_search',
|
||||
GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: this.punctuation
|
||||
GOOGLE_SPEECH_MODEL: 'command_and_search'
|
||||
});
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
if (typeof this.hintsBoost === 'number') {
|
||||
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
|
||||
}
|
||||
}
|
||||
if (this.altLanguages && this.altLanguages.length > 0) {
|
||||
if (this.hints && this.hints.length > 1) opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (this.altLanguages && this.altLanguages.length > 1) {
|
||||
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
if (this.profanityFilter === true) {
|
||||
@@ -235,58 +140,31 @@ class TaskGather extends Task {
|
||||
}
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
else if (['aws', 'polly'].includes(this.vendor)) {
|
||||
else {
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
if (this.vocabularyFilterName) {
|
||||
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
|
||||
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
|
||||
}
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
else if ('microsoft' === this.vendor) {
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
|
||||
'AZURE_REGION': this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption && this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoSpeechDetected.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
this.logger.debug({vars: opts}, 'setting freeswitch vars');
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
|
||||
}
|
||||
|
||||
_startTranscribing(ep) {
|
||||
this.logger.debug({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
interim: this.interim
|
||||
}, 'Gather:_startTranscribing');
|
||||
ep.startTranscription({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
interim: this.interim,
|
||||
interim: this.partialResultCallback ? true : false,
|
||||
}).catch((err) => {
|
||||
const {writeAlerts, AlertType} = this.cs.srf.locals;
|
||||
this.logger.error(err, 'TaskGather:_startTranscribing error');
|
||||
@@ -300,7 +178,6 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
_startTimer() {
|
||||
if (0 === this.timeout) return;
|
||||
assert(!this._timeoutTimer);
|
||||
this.logger.debug(`Gather:_startTimer: timeout ${this.timeout}`);
|
||||
this._timeoutTimer = setTimeout(() => this._resolve('timeout'), this.timeout);
|
||||
@@ -313,102 +190,40 @@ class TaskGather extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_killAudio(cs) {
|
||||
if (!this.sayTask && !this.playTask && this.bargein) {
|
||||
if (this.ep?.connected && !this.playComplete) {
|
||||
this.logger.debug('Gather:_killAudio: killing playback of any audio');
|
||||
this.playComplete = true;
|
||||
this.ep.api('uuid_break', this.ep.uuid)
|
||||
.catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_killAudio() {
|
||||
if (this.sayTask && !this.sayTask.killed) {
|
||||
this.sayTask.removeAllListeners('playDone');
|
||||
this.sayTask.kill(cs);
|
||||
this.sayTask = null;
|
||||
this.sayTask.kill();
|
||||
}
|
||||
if (this.playTask && !this.playTask.killed) {
|
||||
this.playTask.removeAllListeners('playDone');
|
||||
this.playTask.kill(cs);
|
||||
this.playTask = null;
|
||||
this.playTask.kill();
|
||||
}
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, evt) {
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
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');
|
||||
const final = evt.is_final;
|
||||
if (final) {
|
||||
this._resolve('speech', evt);
|
||||
}
|
||||
if (evt.is_final) this._resolve('speech', evt);
|
||||
else {
|
||||
/* google has a measure of stability:
|
||||
https://cloud.google.com/speech-to-text/docs/basics#streaming_responses
|
||||
others do not.
|
||||
*/
|
||||
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
|
||||
|
||||
if (this.bargein && /* isStableEnough && */
|
||||
evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) {
|
||||
if (!this.playComplete) {
|
||||
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
|
||||
}
|
||||
this._killAudio(cs);
|
||||
}
|
||||
if (this.partialResultHook) {
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo));
|
||||
}
|
||||
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'));
|
||||
}
|
||||
}
|
||||
_onEndOfUtterance(cs, ep) {
|
||||
this.logger.debug('TaskGather:_onEndOfUtterance');
|
||||
if (this.bargein && this.minBargeinWordCount === 0) {
|
||||
this._killAudio(cs);
|
||||
}
|
||||
|
||||
this.logger.info('TaskGather:_onEndOfUtterance');
|
||||
if (!this.resolved && !this.killed) {
|
||||
this._startTranscribing(ep);
|
||||
}
|
||||
}
|
||||
|
||||
_onVadDetected(cs, ep) {
|
||||
if (this.bargein && this.minBargeinWordCount === 0) {
|
||||
this.logger.debug('TaskGather:_onVadDetected');
|
||||
this._killAudio(cs);
|
||||
}
|
||||
}
|
||||
|
||||
_onNoSpeechDetected(cs, ep) {
|
||||
this._resolve('timeout');
|
||||
}
|
||||
|
||||
async _resolve(reason, evt) {
|
||||
if (this.resolved) return;
|
||||
this.resolved = true;
|
||||
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
|
||||
clearTimeout(this.interDigitTimer);
|
||||
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.ep.stopTranscription({vendor: this.vendor})
|
||||
@@ -417,25 +232,10 @@ class TaskGather extends Task {
|
||||
|
||||
this._clearTimer();
|
||||
if (reason.startsWith('dtmf')) {
|
||||
if (this.parentTask) this.parentTask.emit('dtmf', evt);
|
||||
else {
|
||||
this.emit('dtmf', evt);
|
||||
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
}
|
||||
await this.performAction({digits: this.digitBuffer});
|
||||
}
|
||||
else if (reason.startsWith('speech')) {
|
||||
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
||||
else {
|
||||
this.emit('transcription', evt);
|
||||
await this.performAction({speech: evt, reason: 'speechDetected'});
|
||||
}
|
||||
}
|
||||
else if (reason.startsWith('timeout')) {
|
||||
if (this.parentTask) this.parentTask.emit('timeout', evt);
|
||||
else {
|
||||
this.emit('timeout', evt);
|
||||
await this.performAction({reason: 'timeout'});
|
||||
}
|
||||
await this.performAction({speech: evt});
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -182,13 +182,12 @@ class Lex extends Task {
|
||||
const type = messages[0].type;
|
||||
if (['PlainText', 'SSML'].includes(type) && msg) {
|
||||
const {srf} = cs;
|
||||
const {stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
|
||||
try {
|
||||
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
const {filePath, servedFromCache} = await synthAudio({
|
||||
text: msg,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
@@ -289,7 +288,7 @@ class Lex extends Task {
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results) {
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, results);
|
||||
const json = await this.cs.requestor.request(hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -64,7 +64,6 @@ class TaskListen extends Task {
|
||||
this.results.dialCallDuration = duration;
|
||||
}
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
this.ep && this._removeListeners(this.ep);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -123,11 +122,6 @@ class TaskListen extends Task {
|
||||
if (this.finishOnKey || this.passDtmf) {
|
||||
ep.on('dtmf', this._dtmfHandler);
|
||||
}
|
||||
|
||||
/* support bi-directional audio */
|
||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
|
||||
}
|
||||
|
||||
_removeListeners(ep) {
|
||||
@@ -137,10 +131,6 @@ 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) {
|
||||
@@ -164,29 +154,6 @@ class TaskListen extends Task {
|
||||
this.logger.info(evt, 'TaskListen:_onConnectFailure');
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onPlayAudio(ep, evt) {
|
||||
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
|
||||
try {
|
||||
const results = await ep.play(evt.file);
|
||||
this.logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
||||
ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.error({err}, 'Error playing file');
|
||||
}
|
||||
}
|
||||
|
||||
_onKillAudio(ep) {
|
||||
this.logger.info('received kill_audio event');
|
||||
ep.api('uuid_break', ep.uuid);
|
||||
}
|
||||
|
||||
_onDisconnect(ep, cs) {
|
||||
this.logger.debug('_onDisconnect: TaskListen terminating task');
|
||||
this.kill(cs);
|
||||
}
|
||||
|
||||
_onError(ep, evt) {
|
||||
this.logger.info(evt, 'TaskListen:_onError');
|
||||
this.notifyTaskDone();
|
||||
|
||||
@@ -17,13 +17,8 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.SipDecline:
|
||||
const TaskSipDecline = require('./sip_decline');
|
||||
return new TaskSipDecline(logger, data, parent);
|
||||
case TaskName.SipRefer:
|
||||
const TaskSipRefer = require('./sip_refer');
|
||||
return new TaskSipRefer(logger, data, parent);
|
||||
case TaskName.Config:
|
||||
const TaskConfig = require('./config');
|
||||
return new TaskConfig(logger, data, parent);
|
||||
case TaskName.Conference:
|
||||
logger.debug({data}, 'Conference verb');
|
||||
const TaskConference = require('./conference');
|
||||
return new TaskConference(logger, data, parent);
|
||||
case TaskName.Dial:
|
||||
@@ -53,9 +48,6 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.Message:
|
||||
const TaskMessage = require('./message');
|
||||
return new TaskMessage(logger, data, parent);
|
||||
case TaskName.Rasa:
|
||||
const TaskRasa = require('./rasa');
|
||||
return new TaskRasa(logger, data, parent);
|
||||
case TaskName.Say:
|
||||
const TaskSay = require('./say');
|
||||
return new TaskSay(logger, data, parent);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const bent = require('bent');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class TaskMessage extends Task {
|
||||
constructor(logger, opts) {
|
||||
@@ -9,11 +8,13 @@ class TaskMessage extends Task {
|
||||
this.preconditions = TaskPreconditions.None;
|
||||
|
||||
this.payload = {
|
||||
message_sid: this.data.message_sid || uuidv4(),
|
||||
carrier: this.data.carrier,
|
||||
message_sid: this.data.message_sid,
|
||||
provider: this.data.provider,
|
||||
to: this.data.to,
|
||||
from: this.data.from,
|
||||
text: this.data.text
|
||||
cc: this.data.cc,
|
||||
text: this.data.text,
|
||||
media: this.data.media
|
||||
};
|
||||
|
||||
}
|
||||
@@ -27,22 +28,20 @@ class TaskMessage extends Task {
|
||||
const {srf, accountSid} = cs;
|
||||
const {res} = cs.callInfo;
|
||||
let payload = this.payload;
|
||||
const actionParams = {message_sid: this.payload.message_sid};
|
||||
|
||||
await super.exec(cs);
|
||||
try {
|
||||
const {getSmpp, dbHelpers} = srf.locals;
|
||||
const {getSBC, getSmpp, dbHelpers} = srf.locals;
|
||||
const {lookupSmppGateways} = dbHelpers;
|
||||
|
||||
this.logger.debug(`looking up gateways for account_sid: ${accountSid}`);
|
||||
this.logger.info(`looking up gateways for account_sid: ${accountSid}`);
|
||||
const r = await lookupSmppGateways(accountSid);
|
||||
let gw, url, relativeUrl;
|
||||
if (r.length > 0) {
|
||||
gw = r.find((o) => 1 === o.sg.outbound && (!this.payload.carrier || o.vc.name === this.payload.carrier));
|
||||
gw = r.find((o) => 1 === o.sg.outbound && (!this.payload.provider || o.vc.name === this.payload.provider));
|
||||
}
|
||||
if (gw) {
|
||||
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
|
||||
url = process.env.K8S ? 'http://smpp' : getSmpp();
|
||||
url = getSmpp();
|
||||
relativeUrl = '/sms';
|
||||
payload = {
|
||||
...payload,
|
||||
@@ -51,75 +50,37 @@ class TaskMessage extends Task {
|
||||
};
|
||||
}
|
||||
else {
|
||||
//TMP: smpp only at the moment, need to add http back in
|
||||
/*
|
||||
this.logger.info({gw, accountSid, carrier: this.payload.carrier},
|
||||
this.logger.info({gw, accountSid, provider: this.payload.provider},
|
||||
'Message:exec - no smpp gateways found to send message');
|
||||
relativeUrl = 'v1/outboundSMS';
|
||||
const sbcAddress = getSBC();
|
||||
if (sbcAddress) url = `http://${sbcAddress}:3000/`;
|
||||
*/
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
message_status: 'no carriers'
|
||||
}).catch((err) => {});
|
||||
if (res) res.sendStatus(404);
|
||||
return;
|
||||
|
||||
//TMP: smpp only at the moment, need to add http back in
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
if (url) {
|
||||
const post = bent(url, 'POST', 'json', 201, 480);
|
||||
const post = bent(url, 'POST', 'json', 201);
|
||||
this.logger.info({payload, url}, 'Message:exec sending outbound SMS');
|
||||
const response = await post(relativeUrl, payload);
|
||||
const {smpp_err_code, carrier, message_id, message} = response;
|
||||
if (smpp_err_code) {
|
||||
this.logger.info({response}, 'SMPP error sending SMS');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
carrier,
|
||||
carrier_message_id: message_id,
|
||||
message_status: 'failure',
|
||||
message_failure_reason: message
|
||||
}).catch((err) => {});
|
||||
if (res) {
|
||||
res.status(480).json({
|
||||
...response,
|
||||
sid: cs.callInfo.messageSid
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const {message_id, carrier} = response;
|
||||
this.logger.info({response}, 'Successfully sent SMS');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
carrier,
|
||||
carrier_message_id: message_id,
|
||||
message_status: 'success',
|
||||
}).catch((err) => {});
|
||||
if (res) {
|
||||
res.status(200).json({
|
||||
sid: cs.callInfo.messageSid,
|
||||
carrierResponse: response
|
||||
});
|
||||
}
|
||||
this.logger.info({response}, 'Successfully sent SMS');
|
||||
if (cs.callInfo.res) {
|
||||
this.logger.info('Message:exec sending 200 OK response to HTTP POST from api server');
|
||||
res.status(200).json({
|
||||
sid: cs.callInfo.messageSid,
|
||||
providerResponse: response
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: action Hook
|
||||
}
|
||||
else {
|
||||
this.logger.info('Message:exec - unable to send SMS as SMPP is not configured on the system');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
message_status: 'smpp configuration error'
|
||||
}).catch((err) => {});
|
||||
if (res) res.status(404).json({message: 'no configured SMS gateways'});
|
||||
this.logger.info('Message:exec - unable to send SMS as there are no available SMS gateways');
|
||||
res.status(422).json({message: 'no configured SMS gateways'});
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'TaskMessage:exec - unexpected error sending SMS');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
message_status: 'system error',
|
||||
message_failure_reason: err.message
|
||||
});
|
||||
if (res) res.status(422).json({message: 'no configured SMS gateways'});
|
||||
this.logger.error(err, 'TaskMessage:exec - Error sending SMS');
|
||||
res.status(422).json({message: 'no configured SMS gateways'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,8 @@ class TaskPlay extends Task {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
try {
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
|
||||
}
|
||||
else await ep.play(this.url);
|
||||
while (!this.killed && this.loop--) {
|
||||
await ep.play(this.url);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
|
||||
@@ -34,13 +30,7 @@ class TaskPlay extends Task {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected && !this.playComplete) {
|
||||
this.logger.debug('TaskPlay:kill - killing audio');
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName} = cs;
|
||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||
}
|
||||
else {
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const bent = require('bent');
|
||||
|
||||
class Rasa extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.prompt = this.data.prompt;
|
||||
this.eventHook = this.data?.eventHook;
|
||||
this.actionHook = this.data?.actionHook;
|
||||
this.post = bent('POST', 'json', 200);
|
||||
}
|
||||
|
||||
get name() { return TaskName.Rasa; }
|
||||
|
||||
get hasReportedFinalAction() {
|
||||
return this.reportedFinalAction || this.isReplacingApplication;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
this.ep = ep;
|
||||
try {
|
||||
/* set event handlers */
|
||||
this.on('transcription', this._onTranscription.bind(this, cs, ep));
|
||||
this.on('timeout', this._onTimeout.bind(this, cs, ep));
|
||||
|
||||
/* start the first gather */
|
||||
this.gatherTask = this._makeGatherTask(this.prompt);
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
.catch((err) => this.logger.info({err}, 'Rasa gather task returned error'));
|
||||
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Rasa error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.debug('Rasa:kill');
|
||||
|
||||
if (!this.hasReportedFinalAction) {
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({rasaResult: 'caller hungup'})
|
||||
.catch((err) => this.logger.info({err}, 'rasa - 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.removeAllListeners();
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_makeGatherTask(prompt) {
|
||||
let opts = {
|
||||
input: ['speech'],
|
||||
timeout: this.data.timeout || 10,
|
||||
recognizer: this.data.recognizer || {
|
||||
vendor: 'default',
|
||||
language: 'default'
|
||||
}
|
||||
};
|
||||
if (prompt) {
|
||||
const sayOpts = this.data.tts ?
|
||||
{text: prompt, synthesizer: this.data.tts} :
|
||||
{text: prompt};
|
||||
|
||||
opts = {
|
||||
...opts,
|
||||
say: sayOpts
|
||||
};
|
||||
}
|
||||
//this.logger.debug({opts}, 'constructing a nested gather object');
|
||||
const gather = makeTask(this.logger, {gather: opts}, this);
|
||||
return gather;
|
||||
}
|
||||
|
||||
async _onTranscription(cs, ep, evt) {
|
||||
//this.logger.debug({evt}, `Rasa: 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('Rasa_onTranscription: event handler for user message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({rasaResult: 'redirect'}, false);
|
||||
if (this.gatherTask) this.gatherTask.kill(cs);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Rasa_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
sender: cs.callSid,
|
||||
message: utterance
|
||||
};
|
||||
this.logger.debug({payload}, 'Rasa:_onTranscription - sending payload to Rasa');
|
||||
const response = await this.post(this.data.url, payload);
|
||||
this.logger.debug({response}, 'Rasa:_onTranscription - got response from Rasa');
|
||||
const botUtterance = Array.isArray(response) ?
|
||||
response.reduce((prev, current) => {
|
||||
return current.text ? `${prev} ${current.text}` : '';
|
||||
}, '') :
|
||||
null;
|
||||
if (botUtterance) {
|
||||
this.logger.debug({botUtterance}, 'Rasa:_onTranscription: got user utterance');
|
||||
this.gatherTask = this._makeGatherTask(botUtterance);
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
.catch((err) => this.logger.info({err}, 'Rasa gather task returned error'));
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'botMessage', message: response})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Rasa_onTranscription: event handler for bot message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({rasaResult: 'redirect'}, false);
|
||||
if (this.gatherTask) this.gatherTask.kill(cs);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Rasa_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Rasa_onTranscription: Error sending user utterance to Rasa - ending task');
|
||||
this.performAction({rasaResult: 'webhookError'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
_onTimeout(cs, ep, evt) {
|
||||
this.logger.debug({evt}, 'Rasa: got timeout');
|
||||
if (!this.hasReportedFinalAction) this.performAction({rasaResult: 'timeout'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = Rasa;
|
||||
@@ -48,7 +48,7 @@ class TaskRestDial extends Task {
|
||||
cs.setDialog(dlg);
|
||||
|
||||
try {
|
||||
const tasks = await cs.requestor.request('verb:hook', this.call_hook, cs.callInfo);
|
||||
const tasks = await cs.requestor.request(this.call_hook, cs.callInfo);
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
||||
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
|
||||
@@ -14,35 +14,19 @@ class TaskSay extends Task {
|
||||
|
||||
get name() { return TaskName.Say; }
|
||||
|
||||
get summary() {
|
||||
for (let i = 0; i < this.text.length; i++) {
|
||||
if (this.text[i].startsWith('silence_stream')) continue;
|
||||
return `${this.name}{text=${this.text[i].slice(0, 15)}${this.text[i].length > 15 ? '...' : ''}}`;
|
||||
}
|
||||
return this.text[0];
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
const {srf} = cs;
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {writeAlerts, AlertType} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
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 vendor = this.synthesizer.vendor || cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language || cs.speechSynthesisLanguage;
|
||||
const voice = this.synthesizer.voice || cs.speechSynthesisVoice;
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
this.logger.info({vendor, language, voice}, 'TaskSay:exec');
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
@@ -56,14 +40,11 @@ class TaskSay extends Task {
|
||||
// synthesize all of the text elements
|
||||
let lastUpdated = false;
|
||||
const filepath = (await Promise.all(this.text.map(async(text) => {
|
||||
if (this.killed) return;
|
||||
if (text.startsWith('silence_stream://')) return text;
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
const {filePath, servedFromCache} = await synthAudio({
|
||||
text,
|
||||
vendor,
|
||||
language,
|
||||
voice,
|
||||
engine,
|
||||
salt,
|
||||
credentials
|
||||
}).catch((err) => {
|
||||
@@ -87,18 +68,10 @@ class TaskSay extends Task {
|
||||
|
||||
this.logger.debug({filepath}, 'synthesized files for tts');
|
||||
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
|
||||
while (!this.killed && this.loop-- && this.ep.connected) {
|
||||
let segment = 0;
|
||||
do {
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, 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]}`);
|
||||
}
|
||||
await ep.play(filepath[segment]);
|
||||
} while (!this.killed && ++segment < filepath.length);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -111,13 +84,7 @@ class TaskSay extends Task {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected) {
|
||||
this.logger.debug('TaskSay:kill - killing audio');
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName} = cs;
|
||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||
}
|
||||
else {
|
||||
this.ep.api('uuid_break', this.ep.uuid);
|
||||
}
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,7 @@ class TaskSipDecline extends Task {
|
||||
res.send(this.data.status, this.data.reason, {
|
||||
headers: this.headers
|
||||
});
|
||||
cs.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Failed,
|
||||
sipStatus: this.data.status,
|
||||
sipReason: this.data.reason
|
||||
});
|
||||
cs.emit('callStatusChange', {callStatus: CallStatus.Failed, sipStatus: this.data.status});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
|
||||
/**
|
||||
* sends a sip REFER to transfer the existing call
|
||||
*/
|
||||
class TaskSipRefer extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.StableCall;
|
||||
|
||||
this.referTo = this.data.referTo;
|
||||
this.referredBy = this.data.referredBy;
|
||||
this.headers = this.data.headers || {};
|
||||
this.eventHook = this.data.eventHook;
|
||||
}
|
||||
|
||||
get name() { return TaskName.SipRefer; }
|
||||
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
const {dlg} = cs;
|
||||
const {referTo, referredBy} = this._normalizeReferHeaders(cs, dlg);
|
||||
|
||||
try {
|
||||
this.notifyHandler = this._handleNotify.bind(this, cs, dlg);
|
||||
dlg.on('notify', this.notifyHandler);
|
||||
const response = await dlg.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
...this.headers,
|
||||
'Refer-To': referTo,
|
||||
'Referred-By': referredBy
|
||||
}
|
||||
});
|
||||
this.referStatus = response.status;
|
||||
this.logger.info(`TaskSipRefer:exec - received ${this.referStatus} to REFER`);
|
||||
|
||||
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
|
||||
if (this.referStatus === 202) {
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
else await this.performAction({refer_status: this.referStatus});
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'TaskSipRefer:exec - error sending REFER');
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
const {dlg} = cs;
|
||||
dlg.off('notify', this.notifyHandler);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _handleNotify(cs, dlg, req, res) {
|
||||
res.send(200);
|
||||
|
||||
const contentType = req.get('Content-Type');
|
||||
this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`);
|
||||
|
||||
if (contentType === 'message/sipfrag') {
|
||||
const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body);
|
||||
if (arr) {
|
||||
const status = arr[1];
|
||||
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
||||
if (this.eventHook) {
|
||||
await cs.requestor.request('verb:hook', this.eventHook, {event: 'transfer-status', call_status: status});
|
||||
}
|
||||
if (status >= 200) {
|
||||
await this.performAction({refer_status: 202, final_referred_call_status: status});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_normalizeReferHeaders(cs, dlg) {
|
||||
let {referTo, referredBy} = this;
|
||||
|
||||
/* get IP address of the SBC to use as hostname if needed */
|
||||
const {host} = parseUri(dlg.remote.uri);
|
||||
|
||||
if (!referTo.startsWith('<') && !referTo.startsWith('sip') && !referTo.startsWith('"')) {
|
||||
/* they may have only provided a phone number/user */
|
||||
referTo = `sip:${referTo}@${host}`;
|
||||
}
|
||||
if (!referredBy) {
|
||||
/* default */
|
||||
referredBy = cs.req?.callingNumber || dlg.local.uri;
|
||||
this.logger.info({referredBy}, 'setting referredby');
|
||||
}
|
||||
if (!referredBy.startsWith('<') && !referredBy.startsWith('sip') && !referredBy.startsWith('"')) {
|
||||
/* they may have only provided a phone number/user */
|
||||
referredBy = `sip:${referredBy}@${host}`;
|
||||
}
|
||||
return {referTo, referredBy};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskSipRefer;
|
||||
@@ -9,43 +9,6 @@
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"sip:refer": {
|
||||
"properties": {
|
||||
"referTo": "string",
|
||||
"referredBy": "string",
|
||||
"headers": "object",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"referTo"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"properties": {
|
||||
"synthesizer": "#synthesizer",
|
||||
"recognizer": "#recognizer",
|
||||
"bargeIn": "#bargeIn"
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"bargeIn": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"actionHook": "object|string",
|
||||
"input": "array",
|
||||
"finishOnKey": "string",
|
||||
"numDigits": "number",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"dtmfBargein": "boolean",
|
||||
"minBargeinWordCount": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
"dequeue": {
|
||||
"properties": {
|
||||
"name": "string",
|
||||
@@ -83,7 +46,7 @@
|
||||
"play": {
|
||||
"properties": {
|
||||
"url": "string",
|
||||
"loop": "number|string",
|
||||
"loop": "number",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
@@ -93,7 +56,7 @@
|
||||
"say": {
|
||||
"properties": {
|
||||
"text": "string|array",
|
||||
"loop": "number|string",
|
||||
"loop": "number",
|
||||
"synthesizer": "#synthesizer",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
@@ -107,21 +70,15 @@
|
||||
"finishOnKey": "string",
|
||||
"input": "array",
|
||||
"numDigits": "number",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"partialResultHook": "object|string",
|
||||
"speechTimeout": "number",
|
||||
"listenDuringPrompt": "boolean",
|
||||
"dtmfBargein": "boolean",
|
||||
"bargein": "boolean",
|
||||
"minBargeinWordCount": "number",
|
||||
"timeout": "number",
|
||||
"recognizer": "#recognizer",
|
||||
"play": "#play",
|
||||
"say": "#say"
|
||||
},
|
||||
"required": [
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"conference": {
|
||||
@@ -131,13 +88,11 @@
|
||||
"startConferenceOnEnter": "boolean",
|
||||
"endConferenceOnExit": "boolean",
|
||||
"maxParticipants": "number",
|
||||
"joinMuted": "boolean",
|
||||
"actionHook": "object|string",
|
||||
"waitHook": "object|string",
|
||||
"statusEvents": "array",
|
||||
"statusHook": "object|string",
|
||||
"enterHook": "object|string",
|
||||
"record": "#record"
|
||||
"enterHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
@@ -149,7 +104,6 @@
|
||||
"answerOnBridge": "boolean",
|
||||
"callerId": "string",
|
||||
"confirmHook": "object|string",
|
||||
"referHook": "object|string",
|
||||
"dialMusic": "string",
|
||||
"dtmfCapture": "object",
|
||||
"dtmfHook": "object|string",
|
||||
@@ -170,10 +124,6 @@
|
||||
"credentials": "object|string",
|
||||
"project": "string",
|
||||
"environment": "string",
|
||||
"region": {
|
||||
"type": "string",
|
||||
"enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"]
|
||||
},
|
||||
"lang": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string",
|
||||
@@ -274,27 +224,6 @@
|
||||
"length"
|
||||
]
|
||||
},
|
||||
"rasa": {
|
||||
"properties": {
|
||||
"url": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"tts": "#synthesizer",
|
||||
"prompt": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"record": {
|
||||
"properties": {
|
||||
"path": "string"
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"properties": {
|
||||
"actionHook": "object|string"
|
||||
@@ -341,6 +270,7 @@
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"transcriptionHook",
|
||||
"recognizer"
|
||||
]
|
||||
},
|
||||
@@ -355,15 +285,12 @@
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"headers": "object",
|
||||
"name": "string",
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
"auth": "#auth",
|
||||
"vmail": "boolean",
|
||||
"tenant": "string",
|
||||
"trunk": "string",
|
||||
"overrideTo": "string"
|
||||
"tenant": "string"
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
@@ -383,14 +310,10 @@
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "polly", "microsoft", "default"]
|
||||
"enum": ["google", "aws", "polly", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"voice": "string",
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"enum": ["standard", "neural"]
|
||||
},
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"enum": ["MALE", "FEMALE", "NEUTRAL"]
|
||||
@@ -404,12 +327,10 @@
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "microsoft", "default"]
|
||||
"enum": ["google", "aws", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"vad": "#vad",
|
||||
"hints": "array",
|
||||
"hintsBoost": "number",
|
||||
"altLanguages": "array",
|
||||
"profanityFilter": "boolean",
|
||||
"interim": "boolean",
|
||||
@@ -446,25 +367,7 @@
|
||||
"mask",
|
||||
"tag"
|
||||
]
|
||||
},
|
||||
"outputFormat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"simple",
|
||||
"detailed"
|
||||
]
|
||||
},
|
||||
"profanityOption": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"masked",
|
||||
"removed",
|
||||
"raw"
|
||||
]
|
||||
},
|
||||
"requestSnr": "boolean",
|
||||
"initialSpeechTimeoutMs": "number",
|
||||
"azureServiceEndpoint": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
@@ -478,15 +381,5 @@
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"vad": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"voiceMs": "number",
|
||||
"mode": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@ class Task extends Emitter {
|
||||
|
||||
this._killInProgress = false;
|
||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||
|
||||
/* used when we play a prompt to a member in conference */
|
||||
this._confPlayCompletionPromise = new Promise((resolve) => this._confPlayCompletionResolver = resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,10 +39,6 @@ class Task extends Emitter {
|
||||
return this.cs;
|
||||
}
|
||||
|
||||
get summary() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.data;
|
||||
}
|
||||
@@ -66,9 +59,7 @@ class Task extends Emitter {
|
||||
kill(cs) {
|
||||
if (this.cs && !this.cs.isConfirmCallSession) this.logger.debug(`${this.name} is being killed`);
|
||||
this._killInProgress = true;
|
||||
|
||||
/* remove reference to parent task or else entangled parent-child tasks will not be gc'ed */
|
||||
setImmediate(() => this.parentTask = null);
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,21 +77,6 @@ class Task extends Emitter {
|
||||
return this._completionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* when a play to conference member completes
|
||||
*/
|
||||
notifyConfPlayDone() {
|
||||
this._confPlayCompletionResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* when a subclass task has launched various async activities and is now simply waiting
|
||||
* for them to complete it should call this method to block until that happens
|
||||
*/
|
||||
awaitConfPlayDone() {
|
||||
return this._confPlayCompletionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* provided as a convenience for tasks, this simply calls CallSession#normalizeUrl
|
||||
*/
|
||||
@@ -111,7 +87,7 @@ class Task extends Emitter {
|
||||
async performAction(results, expectResponse = true) {
|
||||
if (this.actionHook) {
|
||||
const params = results ? Object.assign(results, this.cs.callInfo.toJSON()) : this.cs.callInfo.toJSON();
|
||||
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params);
|
||||
const json = await this.cs.requestor.request(this.actionHook, params);
|
||||
if (expectResponse && json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
@@ -123,62 +99,6 @@ class Task extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
async performHook(cs, hook, results) {
|
||||
const json = await cs.requestor.request('verb:hook', hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.redirect(cs, tasks);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
redirect(cs, tasks) {
|
||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.isReplacingApplication = true;
|
||||
cs.replaceApplication(tasks);
|
||||
}
|
||||
|
||||
async playToConfMember(ep, memberId, confName, confUuid, filepath) {
|
||||
try {
|
||||
this.logger.debug(`Task:playToConfMember - playing ${filepath} to ${confName}:${memberId}`);
|
||||
|
||||
// listen for conference events
|
||||
const handler = this.__onConferenceEvent.bind(this);
|
||||
ep.conn.on('esl::event::CUSTOM::*', handler) ;
|
||||
const response = await ep.api(`conference ${confName} play ${filepath} ${memberId}`);
|
||||
this.logger.debug({response}, 'Task:playToConfMember - api call returned');
|
||||
await this.awaitConfPlayDone();
|
||||
ep.conn.removeListener('esl::event::CUSTOM::*', handler);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `Task:playToConfMember - error playing ${filepath} to ${confName}:${memberId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async killPlayToConfMember(ep, memberId, confName) {
|
||||
try {
|
||||
this.logger.debug(`Task:killPlayToConfMember - killing audio to ${confName}:${memberId}`);
|
||||
const response = await ep.api(`conference ${confName} stop ${memberId}`);
|
||||
this.logger.debug({response}, 'Task:killPlayToConfMember - api call returned');
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `Task:killPlayToConfMember - error killing audio to ${confName}:${memberId}`);
|
||||
}
|
||||
}
|
||||
|
||||
__onConferenceEvent(evt) {
|
||||
const eventName = evt.getHeader('Event-Subclass') ;
|
||||
if (eventName === 'conference::maintenance') {
|
||||
const action = evt.getHeader('Action') ;
|
||||
if (action === 'play-file-member-done') {
|
||||
this.logger.debug('done playing file to conf member');
|
||||
this.notifyConfPlayDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async transferCallToFeatureServer(cs, sipAddress, opts) {
|
||||
const uuid = uuidv4();
|
||||
const {addKey} = cs.srf.locals.dbHelpers;
|
||||
|
||||
@@ -3,7 +3,6 @@ const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
AwsTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
|
||||
@@ -11,7 +10,6 @@ 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);
|
||||
@@ -22,13 +20,8 @@ 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.hintsBoost = recognizer.hintsBoost;
|
||||
this.profanityFilter = recognizer.profanityFilter;
|
||||
this.punctuation = !!recognizer.punctuation;
|
||||
this.enhancedModel = !!recognizer.enhancedModel;
|
||||
@@ -45,13 +38,6 @@ class TaskTranscribe extends Task {
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
this.filterMethod = recognizer.filterMethod;
|
||||
|
||||
/* microsoft options */
|
||||
this.outputFormat = recognizer.outputFormat || 'simple';
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
@@ -67,13 +53,7 @@ class TaskTranscribe extends Task {
|
||||
|
||||
try {
|
||||
if (!this.sttCredentials) {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
vendor: this.vendor
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
// TODO: generate alert (actually should be done by cs.getSpeechCredentials)
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
await this._startTranscribing(cs, ep);
|
||||
@@ -83,7 +63,6 @@ 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);
|
||||
@@ -91,8 +70,6 @@ class TaskTranscribe extends Task {
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
@@ -111,22 +88,14 @@ 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,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep));
|
||||
this._onMaxDurationExceeded.bind(this, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
|
||||
if (this.vendor === 'google') {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
@@ -140,12 +109,7 @@ class TaskTranscribe extends Task {
|
||||
].forEach((arr) => {
|
||||
if (this[arr[0]]) opts[arr[1]] = true;
|
||||
});
|
||||
if (this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (typeof this.hintsBoost === 'number') {
|
||||
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
|
||||
}
|
||||
}
|
||||
if (this.hints.length > 1) opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (this.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
if ('unspecified' !== this.interactionType) {
|
||||
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
|
||||
@@ -200,23 +164,6 @@ class TaskTranscribe extends Task {
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
|
||||
}
|
||||
else if (this.vendor === 'microsoft') {
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
|
||||
'AZURE_REGION': this.sttCredentials.region
|
||||
});
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
if (this.outputFormat !== 'simple') opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
|
||||
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with azure'));
|
||||
}
|
||||
await this._transcribe(ep);
|
||||
}
|
||||
|
||||
@@ -230,36 +177,11 @@ 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 ? nbest.map((n) => {
|
||||
return {
|
||||
confidence: n.Confidence,
|
||||
transcript: n.Display
|
||||
};
|
||||
}) :
|
||||
[
|
||||
{
|
||||
transcript: evt.DisplayText
|
||||
}
|
||||
];
|
||||
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
|
||||
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives
|
||||
};
|
||||
evt = newEvent;
|
||||
}
|
||||
|
||||
if (this.transcriptionHook) {
|
||||
this.cs.requestor.request('verb:hook', 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);
|
||||
}
|
||||
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
|
||||
if (this.killed) {
|
||||
this.logger.debug('TaskTranscribe:_onTranscription exiting after receiving final transcription');
|
||||
this._clearTimer();
|
||||
|
||||
@@ -45,7 +45,6 @@ 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,75 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const Emitter = require('events');
|
||||
const crypto = require('crypto');
|
||||
const timeSeries = require('@jambonz/time-series');
|
||||
let alerter ;
|
||||
|
||||
class BaseRequestor extends Emitter {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super();
|
||||
assert(typeof hook === 'object');
|
||||
|
||||
this.logger = logger;
|
||||
this.url = hook.url;
|
||||
|
||||
this.username = hook.username;
|
||||
this.password = hook.password;
|
||||
this.secret = secret;
|
||||
this.account_sid = account_sid;
|
||||
|
||||
const {stats} = require('../../').srf.locals;
|
||||
this.stats = stats;
|
||||
|
||||
if (!alerter) {
|
||||
alerter = timeSeries(logger, {
|
||||
host: process.env.JAMBONES_TIME_SERIES_HOST,
|
||||
commitSize: 50,
|
||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get Alerter() {
|
||||
return alerter;
|
||||
}
|
||||
|
||||
close() {
|
||||
/* subclass responsibility */
|
||||
}
|
||||
|
||||
_computeSignature(payload, timestamp, secret) {
|
||||
assert(secret);
|
||||
const data = `${timestamp}.${JSON.stringify(payload)}`;
|
||||
return crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(data, 'utf8')
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
_generateSigHeader(payload, secret) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const signature = this._computeSignature(payload, timestamp, secret);
|
||||
const scheme = 'v1';
|
||||
return {
|
||||
'Jambonz-Signature': `t=${timestamp},${scheme}=${signature}`
|
||||
};
|
||||
}
|
||||
|
||||
_isAbsoluteUrl(u) {
|
||||
return typeof u === 'string' &&
|
||||
u.startsWith('https://') || u.startsWith('http://') ||
|
||||
u.startsWith('ws://') || u.startsWith('wss://');
|
||||
}
|
||||
_isRelativeUrl(u) {
|
||||
return typeof u === 'string' && u.startsWith('/');
|
||||
}
|
||||
_roundTrip(startAt) {
|
||||
const diff = process.hrtime(startAt);
|
||||
const time = diff[0] * 1e3 + diff[1] * 1e-6;
|
||||
return time.toFixed(0);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = BaseRequestor;
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"TaskName": {
|
||||
"Cognigy": "cognigy",
|
||||
"Conference": "conference",
|
||||
"Config": "config",
|
||||
"Dequeue": "dequeue",
|
||||
"Dial": "dial",
|
||||
"Dialogflow": "dialogflow",
|
||||
@@ -16,11 +14,9 @@
|
||||
"Message": "message",
|
||||
"Pause": "pause",
|
||||
"Play": "play",
|
||||
"Rasa": "rasa",
|
||||
"Redirect": "redirect",
|
||||
"RestDial": "rest:dial",
|
||||
"SipDecline": "sip:decline",
|
||||
"SipRefer": "sip:refer",
|
||||
"SipNotify": "sip:notify",
|
||||
"SipRedirect": "sip:redirect",
|
||||
"Say": "say",
|
||||
@@ -59,22 +55,13 @@
|
||||
"Transcription": "google_transcribe::transcription",
|
||||
"EndOfUtterance": "google_transcribe::end_of_utterance",
|
||||
"NoAudioDetected": "google_transcribe::no_audio_detected",
|
||||
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded",
|
||||
"VadDetected": "google_transcribe::vad_detected"
|
||||
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded"
|
||||
},
|
||||
"AwsTranscriptionEvents": {
|
||||
"Transcription": "aws_transcribe::transcription",
|
||||
"EndOfTranscript": "aws_transcribe::end_of_transcript",
|
||||
"NoAudioDetected": "aws_transcribe::no_audio_detected",
|
||||
"MaxDurationExceeded": "aws_transcribe::max_duration_exceeded",
|
||||
"VadDetected": "aws_transcribe::vad_detected"
|
||||
},
|
||||
"AzureTranscriptionEvents": {
|
||||
"Transcription": "azure_transcribe::transcription",
|
||||
"StartOfUtterance": "azure_transcribe::start_of_utterance",
|
||||
"EndOfUtterance": "azure_transcribe::end_of_utterance",
|
||||
"NoSpeechDetected": "azure_transcribe::no_speech_detected",
|
||||
"VadDetected": "azure_transcribe::vad_detected"
|
||||
"MaxDurationExceeded": "aws_transcribe::max_duration_exceeded"
|
||||
},
|
||||
"ListenEvents": {
|
||||
"Connect": "mod_audio_fork::connect",
|
||||
@@ -109,16 +96,6 @@
|
||||
"Hangup": "hangup",
|
||||
"Replaced": "replaced"
|
||||
},
|
||||
"HookMsgTypes": [
|
||||
"session:new",
|
||||
"session:reconnect",
|
||||
"session:redirect",
|
||||
"call:status",
|
||||
"queue:status",
|
||||
"verb:hook",
|
||||
"jambonz:error"
|
||||
],
|
||||
"MAX_SIMRINGS": 10,
|
||||
"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"
|
||||
"BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)"
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
const {execSync} = require('child_process');
|
||||
const now = Date.now();
|
||||
const fsInventory = process.env.JAMBONES_FREESWITCH
|
||||
.split(',')
|
||||
.map((fs) => {
|
||||
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
|
||||
const opts = {address: arr[1], port: arr[2], secret: arr[3]};
|
||||
if (arr.length > 4) opts.advertisedAddress = arr[4];
|
||||
if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
|
||||
return opts;
|
||||
});
|
||||
|
||||
const clearChannels = () => {
|
||||
const {logger} = require('../..');
|
||||
const pwd = fsInventory[0].secret;
|
||||
const maxDurationMins = process.env.JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS || 180;
|
||||
|
||||
const calls = execSync(`/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "show calls"`, {encoding: 'utf8'})
|
||||
.split('\n')
|
||||
.filter((line) => line.match(/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{8}/))
|
||||
.map((line) => {
|
||||
const arr = line.split(',');
|
||||
const dt = new Date(arr[2]);
|
||||
const duration = (now - dt.getTime()) / 1000;
|
||||
return {
|
||||
uuid: arr[0],
|
||||
time: arr[2],
|
||||
duration
|
||||
};
|
||||
})
|
||||
.filter((c) => c.duration > 60 * maxDurationMins);
|
||||
|
||||
if (calls.length > 0) {
|
||||
logger.debug(`clearChannels: clearing ${calls.length} old calls longer than ${maxDurationMins} mins`);
|
||||
for (const call of calls) {
|
||||
const cmd = `/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "uuid_kill ${call.uuid}"`;
|
||||
const out = execSync(cmd, {encoding: 'utf8'});
|
||||
logger.debug({out}, 'clearChannels: command output');
|
||||
}
|
||||
}
|
||||
return calls.length;
|
||||
};
|
||||
|
||||
const clearFiles = () => {
|
||||
const {logger} = require('../..');
|
||||
const out = execSync('find /tmp -name "*.mp3" -mtime +2 -exec rm {} \\;');
|
||||
logger.debug({out}, 'clearFiles: command output');
|
||||
};
|
||||
|
||||
|
||||
module.exports = {clearChannels, clearFiles};
|
||||
|
||||
@@ -10,16 +10,6 @@ const sqlSpeechCredentialsForSP = `SELECT *
|
||||
FROM speech_credentials
|
||||
WHERE service_provider_sid =
|
||||
(SELECT service_provider_sid from accounts where account_sid = ?)`;
|
||||
const sqlQueryAccountCarrierByName = `SELECT voip_carrier_sid
|
||||
FROM voip_carriers vc
|
||||
WHERE vc.account_sid = ?
|
||||
AND vc.name = ?`;
|
||||
const sqlQuerySPCarrierByName = `SELECT voip_carrier_sid
|
||||
FROM voip_carriers vc
|
||||
WHERE vc.account_sid IS NULL
|
||||
AND vc.service_provider_sid =
|
||||
(SELECT service_provider_sid from accounts where account_sid = ?)
|
||||
AND vc.name = ?`;
|
||||
|
||||
const speechMapper = (cred) => {
|
||||
const {credential, ...obj} = cred;
|
||||
@@ -31,15 +21,6 @@ const speechMapper = (cred) => {
|
||||
obj.access_key_id = o.access_key_id;
|
||||
obj.secret_access_key = o.secret_access_key;
|
||||
}
|
||||
else if ('microsoft' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
obj.region = o.region;
|
||||
}
|
||||
else if ('wellsaid' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
@@ -56,9 +37,7 @@ module.exports = (logger, srf) => {
|
||||
/* search at the service provider level if we don't find it at the account level */
|
||||
const haveGoogle = speech.find((s) => s.vendor === 'google');
|
||||
const haveAws = speech.find((s) => s.vendor === 'aws');
|
||||
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
|
||||
const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
|
||||
if (!haveGoogle || !haveAws || !haveMicrosoft) {
|
||||
if (!haveGoogle || !haveAws) {
|
||||
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
|
||||
if (r3.length) {
|
||||
if (!haveGoogle) {
|
||||
@@ -69,14 +48,6 @@ module.exports = (logger, srf) => {
|
||||
const aws = r3.find((s) => s.vendor === 'aws');
|
||||
if (aws) speech.push(speechMapper(aws));
|
||||
}
|
||||
if (!haveMicrosoft) {
|
||||
const ms = r3.find((s) => s.vendor === 'microsoft');
|
||||
if (ms) speech.push(speechMapper(ms));
|
||||
}
|
||||
if (!haveWellsaid) {
|
||||
const wellsaid = r3.find((s) => s.vendor === 'wellsaid');
|
||||
if (wellsaid) speech.push(speechMapper(wellsaid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,21 +67,8 @@ module.exports = (logger, srf) => {
|
||||
}
|
||||
};
|
||||
|
||||
const lookupCarrier = async(account_sid, carrierName) => {
|
||||
const pp = pool.promise();
|
||||
try {
|
||||
const [r] = await pp.query(sqlQueryAccountCarrierByName, [account_sid, carrierName]);
|
||||
if (r.length) return r[0].voip_carrier_sid;
|
||||
const [r2] = await pp.query(sqlQuerySPCarrierByName, [account_sid, carrierName]);
|
||||
if (r2.length) return r2[0].voip_carrier_sid;
|
||||
} catch (err) {
|
||||
logger.error({err}, `lookupCarrier: Error ${account_sid}:${carrierName}`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
lookupAccountDetails,
|
||||
updateSpeechCredentialLastUsed,
|
||||
lookupCarrier
|
||||
updateSpeechCredentialLastUsed
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const crypto = require('crypto');
|
||||
const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
|
||||
const algorithm = 'aes-256-ctr';
|
||||
const iv = crypto.randomBytes(16);
|
||||
const secretKey = crypto.createHash('sha256')
|
||||
.update(String(process.env.JWT_SECRET))
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
const bent = require('bent');
|
||||
const parseUrl = require('parse-url');
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
|
||||
function basicAuth(username, password) {
|
||||
if (!username || !password) return {};
|
||||
const creds = `${username}:${password || ''}`;
|
||||
const header = `Basic ${toBase64(creds)}`;
|
||||
return {Authorization: header};
|
||||
}
|
||||
|
||||
|
||||
class HttpRequestor extends BaseRequestor {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super(logger, account_sid, hook, secret);
|
||||
|
||||
this.method = hook.method || 'POST';
|
||||
this.authHeader = basicAuth(hook.username, hook.password);
|
||||
|
||||
const u = parseUrl(this.url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
const baseUrl = this._baseUrl = `${u.protocol}://${u.resource}${myPort}`;
|
||||
|
||||
this.get = bent(baseUrl, 'GET', 'buffer', 200, 201);
|
||||
this.post = bent(baseUrl, 'POST', 'buffer', 200, 201);
|
||||
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
assert(['GET', 'POST'].includes(this.method));
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return this._baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request.
|
||||
* All requests use json bodies.
|
||||
* All requests expect a 200 statusCode on success
|
||||
* @param {object|string} hook - may be a absolute or relative url, or an object
|
||||
* @param {string} [hook.url] - an absolute or relative url
|
||||
* @param {string} [hook.method] - 'GET' or 'POST'
|
||||
* @param {string} [hook.username] - if basic auth is protecting the endpoint
|
||||
* @param {string} [hook.password] - if basic auth is protecting the endpoint
|
||||
* @param {object} [params] - request parameters
|
||||
*/
|
||||
async request(type, hook, params) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
const url = hook.url || hook;
|
||||
const method = hook.method || 'POST';
|
||||
|
||||
assert.ok(url, 'HttpRequestor:request url was not provided');
|
||||
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
|
||||
const {url: urlInfo = hook, method: methodInfo = 'POST'} = hook; // mask user/pass
|
||||
this.logger.debug({url: urlInfo, method: methodInfo, payload}, `HttpRequestor:request ${method} ${url}`);
|
||||
const startAt = process.hrtime();
|
||||
|
||||
let buf;
|
||||
try {
|
||||
const sigHeader = this._generateSigHeader(payload, this.secret);
|
||||
const headers = {...sigHeader, ...this.authHeader};
|
||||
//this.logger.info({url, headers}, 'send webhook');
|
||||
buf = this._isRelativeUrl(url) ?
|
||||
await this.post(url, payload, headers) :
|
||||
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
|
||||
} catch (err) {
|
||||
if (err.statusCode) {
|
||||
this.logger.info({baseUrl: this.baseUrl, url},
|
||||
`web callback returned unexpected status code ${err.statusCode}`);
|
||||
}
|
||||
else {
|
||||
this.logger.error({err, baseUrl: this.baseUrl, url},
|
||||
'web callback returned unexpected error');
|
||||
}
|
||||
let opts = {account_sid: this.account_sid};
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
|
||||
}
|
||||
else if (err.name === 'StatusError') {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_STATUS_FAILURE, url, status: err.statusCode};
|
||||
}
|
||||
else {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url, detail: err.message};
|
||||
}
|
||||
this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
|
||||
|
||||
throw err;
|
||||
}
|
||||
const rtt = this._roundTrip(startAt);
|
||||
if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']);
|
||||
|
||||
if (buf && buf.toString().length > 0) {
|
||||
try {
|
||||
const json = JSON.parse(buf.toString());
|
||||
this.logger.info({response: json}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
|
||||
return json;
|
||||
}
|
||||
catch (err) {
|
||||
//this.logger.debug({err, url, method}, `HttpRequestor:request returned non-JSON content: '${buf.toString()}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HttpRequestor;
|
||||
@@ -20,13 +20,6 @@ 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) {
|
||||
@@ -112,7 +105,6 @@ function installSrfLocals(srf, logger) {
|
||||
const {
|
||||
pool,
|
||||
lookupAppByPhoneNumber,
|
||||
lookupAppByRegex,
|
||||
lookupAppBySid,
|
||||
lookupAppByRealm,
|
||||
lookupAppByTeamsTenant,
|
||||
@@ -143,7 +135,6 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveSet,
|
||||
addToSet,
|
||||
removeFromSet,
|
||||
monitorSet,
|
||||
pushBack,
|
||||
popFront,
|
||||
removeFromList,
|
||||
@@ -167,7 +158,6 @@ function installSrfLocals(srf, logger) {
|
||||
client,
|
||||
pool,
|
||||
lookupAppByPhoneNumber,
|
||||
lookupAppByRegex,
|
||||
lookupAppBySid,
|
||||
lookupAppByRealm,
|
||||
lookupAppByTeamsTenant,
|
||||
@@ -188,7 +178,6 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveSet,
|
||||
addToSet,
|
||||
removeFromSet,
|
||||
monitorSet,
|
||||
pushBack,
|
||||
popFront,
|
||||
removeFromList,
|
||||
|
||||
@@ -5,14 +5,19 @@ 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 {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo}) {
|
||||
super();
|
||||
assert(target.type);
|
||||
|
||||
@@ -26,8 +31,6 @@ class SingleDialer extends Emitter {
|
||||
this.bindings = logger.bindings();
|
||||
|
||||
this.parentCallInfo = callInfo;
|
||||
this.accountInfo = accountInfo;
|
||||
|
||||
this.callGone = false;
|
||||
|
||||
this.callSid = uuidv4();
|
||||
@@ -58,19 +61,7 @@ class SingleDialer extends Emitter {
|
||||
async exec(srf, ms, opts) {
|
||||
opts = opts || {};
|
||||
opts.headers = opts.headers || {};
|
||||
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;
|
||||
opts.headers = {...opts.headers, 'X-Call-Sid': this.callSid};
|
||||
let uri, to;
|
||||
try {
|
||||
switch (this.target.type) {
|
||||
@@ -90,13 +81,18 @@ 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;
|
||||
|
||||
if (this.target.overrideTo) {
|
||||
Object.assign(opts.headers, {
|
||||
'X-Override-To': this.target.overrideTo
|
||||
});
|
||||
// need to send to the SBC registered on
|
||||
const reg = await registrar.query(aor);
|
||||
if (reg) {
|
||||
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':
|
||||
@@ -150,7 +146,6 @@ 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,
|
||||
@@ -164,14 +159,10 @@ class SingleDialer extends Emitter {
|
||||
callId: this.callInfo.callId
|
||||
});
|
||||
this.inviteInProgress = req;
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Trying,
|
||||
sipStatus: 100,
|
||||
sipReason: 'Trying'
|
||||
});
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
},
|
||||
cbProvisional: (prov) => {
|
||||
const status = {sipStatus: prov.status, sipReason: prov.reason};
|
||||
const status = {sipStatus: prov.status};
|
||||
if ([180, 183].includes(prov.status) && prov.body) {
|
||||
if (status.callStatus !== CallStatus.EarlyMedia) {
|
||||
status.callStatus = CallStatus.EarlyMedia;
|
||||
@@ -186,63 +177,34 @@ class SingleDialer extends Emitter {
|
||||
await connectStream(this.dlg.remote.sdp);
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.inviteInProgress = null;
|
||||
this.emit('callStatusChange', {
|
||||
sipStatus: 200,
|
||||
sipReason: 'OK',
|
||||
callStatus: CallStatus.InProgress
|
||||
});
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`);
|
||||
const connectTime = this.dlg.connectTime = moment();
|
||||
|
||||
/* race condition: we were killed just as call was answered */
|
||||
if (this.killed) {
|
||||
this.logger.info(`SingleDialer:exec race condition - we were killed as call connected: ${this.callSid}`);
|
||||
const duration = moment().diff(connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Completed,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Request Terminated',
|
||||
duration
|
||||
});
|
||||
if (this.ep) this.ep.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dlg
|
||||
.on('destroy', () => {
|
||||
const duration = moment().diff(connectTime, 'seconds');
|
||||
this.logger.debug('SingleDialer:exec called party hung up');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.ep && this.ep.destroy();
|
||||
this.ep.destroy();
|
||||
})
|
||||
.on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas'))
|
||||
.on('modify', async(req, res) => {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
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');
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
})
|
||||
.on('refer', (req, res) => {
|
||||
this.emit('refer', this.callInfo, req, res);
|
||||
});
|
||||
|
||||
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;
|
||||
status.sipReason = err.reason;
|
||||
if (err.status === 487) status.callStatus = CallStatus.NoAnswer;
|
||||
else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy;
|
||||
this.logger.info(`SingleDialer:exec outdial failure ${err.status}`);
|
||||
@@ -284,7 +246,7 @@ class SingleDialer extends Emitter {
|
||||
async _executeApp(confirmHook) {
|
||||
try {
|
||||
// retrieve set of tasks
|
||||
const tasks = await this.requestor.request(confirmHook, this.callInfo.toJSON());
|
||||
const tasks = await this.requestor.request(confirmHook, this.callInfo);
|
||||
|
||||
// verify it contains only allowed verbs
|
||||
const allowedTasks = tasks.filter((task) => {
|
||||
@@ -322,59 +284,30 @@ class SingleDialer extends Emitter {
|
||||
this.logger = logger;
|
||||
this.adulting = true;
|
||||
this.emit('adulting');
|
||||
if (this.ep) {
|
||||
await this.ep.unbridge()
|
||||
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
|
||||
this.ep.play('silence_stream://1000');
|
||||
}
|
||||
else {
|
||||
await this.reAnchorMedia();
|
||||
}
|
||||
await this.ep.unbridge()
|
||||
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
|
||||
this.ep.play('silence_stream://1000');
|
||||
const cs = new AdultingCallSession({
|
||||
logger: this.logger,
|
||||
singleDialer: this,
|
||||
application,
|
||||
callInfo: this.callInfo,
|
||||
accountInfo: this.accountInfo,
|
||||
tasks
|
||||
});
|
||||
cs.exec();
|
||||
return cs;
|
||||
}
|
||||
|
||||
async releaseMediaToSBC(remoteSdp, localSdp) {
|
||||
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
|
||||
const sdp = stripCodecs(this.logger, remoteSdp, localSdp) || remoteSdp;
|
||||
await this.dlg.modify(sdp, {
|
||||
headers: {
|
||||
'X-Reason': 'release-media'
|
||||
}
|
||||
});
|
||||
this.ep.destroy()
|
||||
.then(() => this.ep = null)
|
||||
.catch((err) => this.logger.error({err}, 'SingleDialer:releaseMediaToSBC: Error destroying endpoint'));
|
||||
}
|
||||
|
||||
async reAnchorMedia() {
|
||||
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
await this.dlg.modify(this.ep.local.sdp, {
|
||||
headers: {
|
||||
'X-Reason': 'anchor-media'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
|
||||
if (this.callInfo) {
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.requestor.request('call:status', this.application.call_status_hook, this.callInfo.toJSON());
|
||||
this.requestor.request(this.application.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
this.logger.info(err, `SingleDialer:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
@@ -387,9 +320,9 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo}) {
|
||||
const myOpts = deepcopy(opts);
|
||||
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo, accountInfo});
|
||||
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo});
|
||||
sd.exec(srf, ms, myOpts);
|
||||
return sd;
|
||||
}
|
||||
|
||||
@@ -100,15 +100,15 @@ class Requestor {
|
||||
|
||||
assert.ok(url, 'Requestor:request url was not provided');
|
||||
assert.ok, (['GET', 'POST'].includes(method), `Requestor:request method must be 'GET' or 'POST' not ${method}`);
|
||||
const {url: urlInfo = hook, method: methodInfo = 'POST'} = hook; // mask user/pass
|
||||
this.logger.debug({url: urlInfo, method: methodInfo, payload}, `Requestor:request ${method} ${url}`);
|
||||
|
||||
this.logger.debug({hook, payload}, `Requestor:request ${method} ${url}`);
|
||||
const startAt = process.hrtime();
|
||||
|
||||
let buf;
|
||||
try {
|
||||
const sigHeader = generateSigHeader(payload, this.secret);
|
||||
const headers = {...sigHeader, ...this.authHeader};
|
||||
//this.logger.info({url, headers}, 'send webhook');
|
||||
this.logger.info({url, headers}, 'send webhook');
|
||||
buf = isRelativeUrl(url) ?
|
||||
await this.post(url, payload, headers) :
|
||||
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
const assert = require('assert');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants');
|
||||
const noopLogger = {info: () => {}, error: () => {}};
|
||||
const {LifeCycleEvents} = require('./constants');
|
||||
const Emitter = require('events');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const noopLogger = {info: () => {}, error: () => {}};
|
||||
|
||||
module.exports = (logger) => {
|
||||
logger = logger || noopLogger;
|
||||
let idxSbc = 0;
|
||||
let sbcs = [];
|
||||
|
||||
if (process.env.JAMBONES_SBCS) {
|
||||
sbcs = process.env.JAMBONES_SBCS
|
||||
.split(',')
|
||||
.map((sbc) => sbc.trim());
|
||||
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
}
|
||||
else if (process.env.K8S && process.env.K8S_SBC_SIP_SERVICE_NAME) {
|
||||
sbcs = [`${process.env.K8S_SBC_SIP_SERVICE_NAME}:5060`];
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
}
|
||||
assert.ok(process.env.JAMBONES_SBCS, 'missing JAMBONES_SBCS env var');
|
||||
const sbcs = process.env.JAMBONES_SBCS
|
||||
.split(',')
|
||||
.map((sbc) => sbc.trim());
|
||||
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
|
||||
// listen for SNS lifecycle changes
|
||||
let lifecycleEmitter = new Emitter();
|
||||
@@ -32,10 +25,6 @@ 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;
|
||||
@@ -76,6 +65,7 @@ module.exports = (logger) => {
|
||||
})();
|
||||
}
|
||||
|
||||
// send OPTIONS pings to SBCs
|
||||
async function pingProxies(srf) {
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
|
||||
@@ -98,40 +88,18 @@ 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);
|
||||
|
||||
// initial ping once we are up
|
||||
setTimeout(async() => {
|
||||
// OPTIONS ping the SBCs from each feature server every 60 seconds
|
||||
setInterval(() => {
|
||||
const {srf} = require('../..');
|
||||
pingProxies(srf);
|
||||
}, 20000);
|
||||
|
||||
// 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}`);
|
||||
});
|
||||
}
|
||||
|
||||
pingProxies(srf);
|
||||
}, 1000);
|
||||
}
|
||||
// initial ping once we are up
|
||||
setTimeout(() => {
|
||||
const {srf} = require('../..');
|
||||
pingProxies(srf);
|
||||
}, 1000);
|
||||
|
||||
return {
|
||||
lifecycleEmitter,
|
||||
|
||||
13
lib/utils/select-sbc.js
Normal file
13
lib/utils/select-sbc.js
Normal file
@@ -0,0 +1,13 @@
|
||||
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}`;
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module.exports = function(tasks) {
|
||||
return `[${tasks.map((t) => t.summary).join(',')}]`;
|
||||
return `[${tasks.map((t) => t.name).join(',')}]`;
|
||||
};
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const short = require('short-uuid');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const Websocket = require('ws');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const HttpRequestor = require('./http-requestor');
|
||||
const MAX_RECONNECTS = 5;
|
||||
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
|
||||
|
||||
class WsRequestor extends BaseRequestor {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super(logger, account_sid, hook, secret);
|
||||
this.connections = 0;
|
||||
this.messagesInFlight = new Map();
|
||||
this.maliciousClient = false;
|
||||
this.closedByUs = false;
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
|
||||
this.on('socket-closed', this._onSocketClosed.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON payload over the websocket. If this is the first request,
|
||||
* open the websocket.
|
||||
* All requests expect an ack message in response
|
||||
* @param {object|string} hook - may be a absolute or relative url, or an object
|
||||
* @param {string} [hook.url] - an absolute or relative url
|
||||
* @param {string} [hook.method] - 'GET' or 'POST'
|
||||
* @param {string} [hook.username] - if basic auth is protecting the endpoint
|
||||
* @param {string} [hook.password] - if basic auth is protecting the endpoint
|
||||
* @param {object} [params] - request parameters
|
||||
*/
|
||||
async request(type, hook, params) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
const url = hook.url || hook;
|
||||
|
||||
if (this.maliciousClient) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'session:new') this.call_sid = params.callSid;
|
||||
|
||||
/* if we have an absolute url, and it is http then do a standard webhook */
|
||||
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
|
||||
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
|
||||
const requestor = new HttpRequestor(this.logger, this.account_sid, hook, this.secret);
|
||||
return requestor.request(type, hook, params);
|
||||
}
|
||||
|
||||
/* connect if necessary */
|
||||
if (!this.ws) {
|
||||
if (this.connections >= MAX_RECONNECTS) {
|
||||
throw new Error(`max attempts connecting to ${this.url}`);
|
||||
}
|
||||
try {
|
||||
const startAt = process.hrtime();
|
||||
await this._connect();
|
||||
const rtt = this._roundTrip(startAt);
|
||||
this.stats.histogram('app.hook.connect_time', rtt, ['hook_type:app']);
|
||||
} catch (err) {
|
||||
this.logger.info({url, err}, 'WsRequestor:request - failed connecting');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
assert(this.ws);
|
||||
|
||||
/* prepare and send message */
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||
|
||||
const msgid = short.generate();
|
||||
const obj = {
|
||||
type,
|
||||
msgid,
|
||||
call_sid: this.call_sid,
|
||||
hook: type === 'verb:hook' ? url : undefined,
|
||||
data: {...payload}
|
||||
};
|
||||
|
||||
//this.logger.debug({obj}, `websocket: sending (${url})`);
|
||||
|
||||
/* simple notifications */
|
||||
if (['call:status', 'jambonz:error'].includes(type)) {
|
||||
this.ws.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/* messages that require an ack */
|
||||
return new Promise((resolve, reject) => {
|
||||
/* give the far end a reasonable amount of time to ack our message */
|
||||
const timer = setTimeout(() => {
|
||||
const {failure} = this.messagesInFlight.get(msgid);
|
||||
failure && failure(`timeout from far end for msgid ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
}, RESPONSE_TIMEOUT_MS);
|
||||
|
||||
/* save the message info for reply */
|
||||
const startAt = process.hrtime();
|
||||
this.messagesInFlight.set(msgid, {
|
||||
success: (response) => {
|
||||
clearTimeout(timer);
|
||||
const rtt = this._roundTrip(startAt);
|
||||
this.logger.info({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
|
||||
this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']);
|
||||
resolve(response);
|
||||
},
|
||||
failure: (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
/* send the message */
|
||||
this.ws.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.logger.info('WsRequestor:close closing socket');
|
||||
this.closedByUs = true;
|
||||
try {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws.removeAllListeners();
|
||||
}
|
||||
|
||||
for (const [msgid, obj] of this.messagesInFlight) {
|
||||
obj.failure(`abandoning msgid ${msgid} since we have closed the socket`);
|
||||
}
|
||||
this.messagesInFlight.clear();
|
||||
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'WsRequestor: Error closing socket');
|
||||
}
|
||||
}
|
||||
|
||||
_connect() {
|
||||
assert(!this.ws);
|
||||
return new Promise((resolve, reject) => {
|
||||
let opts = {
|
||||
followRedirects: true,
|
||||
maxRedirects: 2,
|
||||
handshakeTimeout: 1000,
|
||||
maxPayload: 8096,
|
||||
};
|
||||
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
|
||||
|
||||
this
|
||||
.once('ready', (ws) => {
|
||||
this.ws = ws;
|
||||
this.removeAllListeners('not-ready');
|
||||
if (this.connections++ > 0) this.request('session:reconnect', this.url);
|
||||
resolve();
|
||||
})
|
||||
.once('not-ready', (err) => {
|
||||
this.removeAllListeners('ready');
|
||||
reject(err);
|
||||
});
|
||||
const ws = new Websocket(this.url, ['ws.jambonz.org'], opts);
|
||||
this._setHandlers(ws);
|
||||
});
|
||||
}
|
||||
|
||||
_setHandlers(ws) {
|
||||
ws
|
||||
.once('open', this._onOpen.bind(this, ws))
|
||||
.once('close', this._onClose.bind(this))
|
||||
.on('message', this._onMessage.bind(this))
|
||||
.once('unexpected-response', this._onUnexpectedResponse.bind(this, ws))
|
||||
.on('error', this._onError.bind(this));
|
||||
}
|
||||
|
||||
_onError(err) {
|
||||
if (this.connections > 0) {
|
||||
this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
|
||||
}
|
||||
else this.emit('not-ready', err);
|
||||
}
|
||||
|
||||
_onOpen(ws) {
|
||||
if (this.ws) this.logger.info({old_ws: this.ws._socket.address()}, 'WsRequestor:_onOpen');
|
||||
assert(!this.ws);
|
||||
this.emit('ready', ws);
|
||||
this.logger.info({url: this.url}, 'WsRequestor - successfully connected');
|
||||
}
|
||||
|
||||
_onClose() {
|
||||
if (this.connections > 0) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
||||
this.emit('socket-closed');
|
||||
}
|
||||
this.ws && this.ws.removeAllListeners();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
_onUnexpectedResponse(ws, req, res) {
|
||||
assert(!this.ws);
|
||||
this.logger.info({
|
||||
headers: res.headers,
|
||||
statusCode: res.statusCode,
|
||||
statusMessage: res.statusMessage
|
||||
}, 'WsRequestor - unexpected response');
|
||||
this.emit('connection-failure');
|
||||
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
|
||||
}
|
||||
|
||||
_onSocketClosed() {
|
||||
this.ws = null;
|
||||
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedByUs) {
|
||||
setTimeout(this._connect.bind(this), 500);
|
||||
}
|
||||
}
|
||||
|
||||
_onMessage(content, isBinary) {
|
||||
if (this.isBinary) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:_onMessage - discarding binary message');
|
||||
this.maliciousClient = true;
|
||||
this.ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
/* messages must be JSON format */
|
||||
try {
|
||||
const obj = JSON.parse(content);
|
||||
const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
|
||||
|
||||
this.logger.debug({obj}, 'WsRequestor:request websocket: received');
|
||||
assert.ok(type, 'type property not supplied');
|
||||
|
||||
switch (type) {
|
||||
case 'ack':
|
||||
assert.ok(msgid, 'msgid not supplied');
|
||||
this._recvAck(msgid, data);
|
||||
break;
|
||||
|
||||
case 'command':
|
||||
assert.ok(command, 'command property not supplied');
|
||||
assert.ok(data, 'data property not supplied');
|
||||
this._recvCommand(msgid, command, call_sid, queueCommand, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert.ok(false, `invalid type property: ${type}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'WsRequestor:_onMessage - invalid incoming message');
|
||||
}
|
||||
}
|
||||
|
||||
_recvAck(msgid, data) {
|
||||
const obj = this.messagesInFlight.get(msgid);
|
||||
if (!obj) {
|
||||
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
const {success} = obj;
|
||||
success && success(data);
|
||||
}
|
||||
|
||||
_recvCommand(msgid, command, call_sid, queueCommand, data) {
|
||||
// TODO: validate command
|
||||
this.logger.info({msgid, command, call_sid, queueCommand, data}, 'received command');
|
||||
this.emit('command', {msgid, command, call_sid, queueCommand, data});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WsRequestor;
|
||||
3242
package-lock.json
generated
3242
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.4",
|
||||
"version": "0.3.1",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -21,36 +21,31 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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=error 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/ ",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cognigy/socket-client": "^4.5.5",
|
||||
"@jambonz/db-helpers": "^0.6.16",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/db-helpers": "^0.6.13",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.26",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"aws-sdk": "^2.1073.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.1",
|
||||
"@jambonz/stats-collector": "^0.1.5",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"aws-sdk": "^2.846.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.2",
|
||||
"debug": "^4.3.1",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^2.0.13",
|
||||
"drachtio-srf": "^4.4.61",
|
||||
"drachtio-fsmrf": "^2.0.7",
|
||||
"drachtio-srf": "^4.4.50",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.13.4",
|
||||
"short-uuid": "^4.2.0",
|
||||
"parse-url": "^5.0.2",
|
||||
"pino": "^6.11.2",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
"ws": "^8.5.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,9 +55,5 @@
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
"utf-8-validate": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,8 +587,7 @@ DROP TABLE IF EXISTS `speech_credentials`;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `speech_credentials` (
|
||||
`speech_credential_sid` char(36) NOT NULL,
|
||||
`service_provider_sid` CHAR(36),
|
||||
`account_sid` char(36) NOT NULL,
|
||||
`account_sid` char(36) NOT NULL,
|
||||
`vendor` varchar(255) NOT NULL,
|
||||
`credential` VARCHAR(8192) NOT NULL,
|
||||
`use_for_tts` tinyint(1) DEFAULT '1',
|
||||
@@ -612,7 +611,7 @@ CREATE TABLE `speech_credentials` (
|
||||
|
||||
LOCK TABLES `speech_credentials` WRITE;
|
||||
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
|
||||
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
|
||||
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
|
||||
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
|
||||
@@ -20,16 +20,12 @@ DROP TABLE IF EXISTS lcr_routes;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_sip_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_smpp_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_carriers;
|
||||
|
||||
DROP TABLE IF EXISTS account_offers;
|
||||
|
||||
DROP TABLE IF EXISTS products;
|
||||
|
||||
DROP TABLE IF EXISTS schema_version;
|
||||
|
||||
DROP TABLE IF EXISTS api_keys;
|
||||
|
||||
DROP TABLE IF EXISTS sbc_addresses;
|
||||
@@ -152,20 +148,6 @@ predefined_carrier_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (predefined_sip_gateway_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE predefined_smpp_gateways
|
||||
(
|
||||
predefined_smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. ',
|
||||
port INTEGER NOT NULL DEFAULT 2775 COMMENT 'smpp signaling port',
|
||||
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound SMS from the gateway',
|
||||
outbound BOOLEAN NOT NULL COMMENT 'i',
|
||||
netmask INTEGER NOT NULL DEFAULT 32,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT 1,
|
||||
use_tls BOOLEAN DEFAULT 0,
|
||||
predefined_carrier_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (predefined_smpp_gateway_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE products
|
||||
(
|
||||
product_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -192,11 +174,6 @@ stripe_product_id VARCHAR(56) NOT NULL,
|
||||
PRIMARY KEY (account_offer_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE schema_version
|
||||
(
|
||||
version VARCHAR(16)
|
||||
);
|
||||
|
||||
CREATE TABLE api_keys
|
||||
(
|
||||
api_key_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -443,10 +420,6 @@ CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefin
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
|
||||
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
|
||||
|
||||
CREATE INDEX predefined_smpp_gateway_sid_idx ON predefined_smpp_gateways (predefined_smpp_gateway_sid);
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_smpp_gateways (predefined_carrier_sid);
|
||||
ALTER TABLE predefined_smpp_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk_1 (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
|
||||
|
||||
CREATE INDEX product_sid_idx ON products (product_sid);
|
||||
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
|
||||
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
|
||||
@@ -572,4 +545,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
@@ -9,7 +9,6 @@ networks:
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
platform: linux/x86_64
|
||||
ports:
|
||||
- "3360:3306"
|
||||
environment:
|
||||
@@ -124,7 +123,7 @@ services:
|
||||
ipv4_address: 172.38.0.63
|
||||
|
||||
influxdb:
|
||||
image: influxdb:1.8
|
||||
image: influxdb:1.8-alpine
|
||||
ports:
|
||||
- "8086:8086"
|
||||
networks:
|
||||
|
||||
Reference in New Issue
Block a user