Compare commits

..

2 Commits

Author SHA1 Message Date
Dave Horton
3ab4f3fdf9 linting 2022-06-18 15:04:36 -04:00
akirilyuk
a92e9d0f3e add defaults to rest call payload (#115)
Co-authored-by: akirilyuk <a.kirilyuk@cognigy.com>
2022-06-18 15:04:11 -04:00
102 changed files with 5615 additions and 15849 deletions

View File

@@ -1,15 +1,16 @@
name: CI name: CI
on: [push, pull_request] on:
push:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-node@v3 - uses: actions/setup-node@v1
with: with:
node-version: 16 node-version: 14
- run: npm ci - run: npm ci
- run: npm run jslint - run: npm run jslint
- run: docker pull drachtio/sipp - run: docker pull drachtio/sipp
@@ -19,5 +20,3 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }} AWS_REGION: ${{ secrets.AWS_REGION }}
MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }}

View File

@@ -20,7 +20,7 @@ jobs:
if: github.event_name == 'push' if: github.event_name == 'push'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Build image - name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME run: docker build . --file Dockerfile --tag $IMAGE_NAME

3
.gitignore vendored
View File

@@ -40,5 +40,4 @@ examples/*
ecosystem.config.js ecosystem.config.js
.vscode .vscode
test/credentials/*.json test/credentials/*.json
run-tests.sh run-tests.sh
run-coverage.sh

View File

@@ -1,23 +1,10 @@
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base FROM node:lts-slim
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/ WORKDIR /opt/app/
FROM base as build
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci
RUN npm prune
COPY . . COPY . /opt/app
FROM base
COPY --from=build /opt/app /opt/app/
ARG NODE_ENV ARG NODE_ENV
ENV NODE_ENV $NODE_ENV ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ] CMD [ "npm", "start" ]

View File

@@ -20,7 +20,6 @@ Configuration is provided via environment variables:
|ENABLE_METRICS| if 1, metrics will be generated|no| |ENABLE_METRICS| if 1, metrics will be generated|no|
|GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes| |GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes| |HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no|
|JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes| |JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no| |JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|JAMBONES_MYSQL_HOST| mysql host|yes| |JAMBONES_MYSQL_HOST| mysql host|yes|

81
app.js
View File

@@ -15,9 +15,12 @@ const tracer = require('./tracer')(process.env.JAMBONES_OTEL_SERVICE_NAME || 'ja
const api = require('@opentelemetry/api'); const api = require('@opentelemetry/api');
srf.locals = {...srf.locals, otel: {tracer, api}}; srf.locals = {...srf.locals, otel: {tracer, api}};
const opts = {level: process.env.JAMBONES_LOGLEVEL || 'info'}; const PORT = process.env.HTTP_PORT || 3000;
const pino = require('pino'); const opts = {
const logger = pino(opts, pino.destination({sync: false})); timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const logger = require('pino')(opts);
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants'); const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
const installSrfLocals = require('./lib/utils/install-srf-locals'); const installSrfLocals = require('./lib/utils/install-srf-locals');
installSrfLocals(srf, logger); installSrfLocals(srf, logger);
@@ -25,15 +28,24 @@ installSrfLocals(srf, logger);
const { const {
initLocals, initLocals,
createRootSpan, createRootSpan,
handleSipRec,
getAccountDetails, getAccountDetails,
normalizeNumbers, normalizeNumbers,
retrieveApplication, retrieveApplication,
invokeWebCallback invokeWebCallback
} = require('./lib/middleware')(srf, logger); } = require('./lib/middleware')(srf, logger);
// HTTP
const express = require('express');
const helmet = require('helmet');
const app = express();
Object.assign(app.locals, {
logger,
srf
});
const httpRoutes = require('./lib/http-routes');
const InboundCallSession = require('./lib/session/inbound-call-session'); const InboundCallSession = require('./lib/session/inbound-call-session');
const SipRecCallSession = require('./lib/session/siprec-call-session');
if (process.env.DRACHTIO_HOST) { if (process.env.DRACHTIO_HOST) {
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET }); srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
@@ -56,20 +68,31 @@ if (process.env.NODE_ENV === 'test') {
srf.use('invite', [ srf.use('invite', [
initLocals, initLocals,
createRootSpan, createRootSpan,
handleSipRec,
getAccountDetails, getAccountDetails,
normalizeNumbers, normalizeNumbers,
retrieveApplication, retrieveApplication,
invokeWebCallback invokeWebCallback
]); ]);
srf.invite(async(req, res) => { srf.invite((req, res) => {
const isSipRec = !!req.locals.siprec; const session = new InboundCallSession(req, res);
const session = isSipRec ? new SipRecCallSession(req, res) : new InboundCallSession(req, res);
if (isSipRec) await session.answerSipRecCall();
session.exec(); session.exec();
}); });
// HTTP
app.use(helmet());
app.use(helmet.hidePoweredBy());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use('/', httpRoutes);
app.use((err, req, res, next) => {
logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
const httpServer = app.listen(PORT);
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
const sessionTracker = srf.locals.sessionTracker = require('./lib/session/session-tracker'); const sessionTracker = srf.locals.sessionTracker = require('./lib/session/session-tracker');
sessionTracker.on('idle', () => { sessionTracker.on('idle', () => {
if (srf.locals.lifecycleEmitter.operationalState === LifeCycleEvents.ScaleIn) { if (srf.locals.lifecycleEmitter.operationalState === LifeCycleEvents.ScaleIn) {
@@ -77,54 +100,34 @@ sessionTracker.on('idle', () => {
srf.locals.lifecycleEmitter.scaleIn(); srf.locals.lifecycleEmitter.scaleIn();
} }
}); });
const getCount = () => sessionTracker.count; const getCount = () => sessionTracker.count;
const healthCheck = require('@jambonz/http-health-check'); const healthCheck = require('@jambonz/http-health-check');
let httpServer; healthCheck({app, logger, path: '/', fn: getCount});
const createHttpListener = require('./lib/utils/http-listener');
createHttpListener(logger, srf)
.then(({server, app}) => {
httpServer = server;
healthCheck({app, logger, path: '/', fn: getCount});
return {server, app};
})
.catch((err) => {
logger.error(err, 'Error creating http listener');
});
setInterval(() => { setInterval(() => {
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count); srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
}, 20000); }, 5000);
const disconnect = () => { const disconnect = () => {
return new Promise ((resolve) => { return new Promise ((resolve) => {
httpServer?.on('close', resolve); httpServer.on('close', resolve);
httpServer?.close(); httpServer.close();
srf.disconnect(); srf.disconnect();
srf.locals.mediaservers.forEach((ms) => ms.disconnect()); srf.locals.mediaservers.forEach((ms) => ms.disconnect());
}); });
}; };
process.on('SIGUSR2', handle);
process.on('SIGTERM', handle); process.on('SIGTERM', handle);
function handle(signal) { function handle(signal) {
const {removeFromSet} = srf.locals.dbHelpers; const {removeFromSet} = srf.locals.dbHelpers;
srf.locals.disabled = true;
logger.info(`got signal ${signal}`);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`; const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
if (setName && srf.locals.localSipAddress) { logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`); removeFromSet(setName, srf.locals.localSipAddress);
removeFromSet(setName, srf.locals.localSipAddress);
}
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID); removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
if (process.env.K8S) { srf.locals.disabled = true;
srf.locals.lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
}
if (getCount() === 0) {
logger.info('no calls in progress, exiting');
process.exit(0);
}
} }
if (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS) { if (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS) {

View File

@@ -1,52 +0,0 @@
{
"en-US": [
"call has been forwarded",
"at the beep",
"at the tone",
"leave a message",
"leave me a message",
"not available right now",
"not available to take your call",
"can't take your call",
"I will get back to you",
"I'll get back to you",
"we will get back to you",
"we are unable",
"we are not available"
],
"es-ES": [
"le pasamos la llamada",
"después del bip",
"después del tono",
"deja un mensaje",
"déjame un mensaje",
"no estamos disponibles",
"no estoy disponible",
"ahora no puedo",
"no puedo contestar",
"no le puedo contestar",
"me pondré en contacto",
"nos pondremos en contacto",
"ahora no estamos disponibles",
"no estamos disponibles"
],
"ca-ES": [
"passem la seva trucada",
"després del bip",
"després del to",
"deixi un missatge",
"deixa un missatge",
"deixim un missatge",
"no estem disponibles",
"no estem a l'oficina",
"no estic disponible",
"ara no puc",
"no puc contestar",
"no puc respondre",
"no li puc respondre",
"em posaré en contacte",
"ens posarem en contacto",
"ara no estem disponibles",
"no hi som"
]
}

View File

@@ -3,7 +3,7 @@ const makeTask = require('../../tasks/make_task');
const RestCallSession = require('../../session/rest-call-session'); const RestCallSession = require('../../session/rest-call-session');
const CallInfo = require('../../session/call-info'); const CallInfo = require('../../session/call-info');
const {CallDirection, CallStatus} = require('../../utils/constants'); const {CallDirection, CallStatus} = require('../../utils/constants');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const SipError = require('drachtio-srf').SipError; const SipError = require('drachtio-srf').SipError;
const sysError = require('./error'); const sysError = require('./error');
const HttpRequestor = require('../../utils/http-requestor'); const HttpRequestor = require('../../utils/http-requestor');
@@ -41,8 +41,7 @@ router.post('/', async(req, res) => {
'X-Jambonz-Routing': target.type, 'X-Jambonz-Routing': target.type,
'X-Jambonz-FS-UUID': srf.locals.fsUUID, 'X-Jambonz-FS-UUID': srf.locals.fsUUID,
'X-Call-Sid': callSid, 'X-Call-Sid': callSid,
'X-Account-Sid': accountSid, 'X-Account-Sid': accountSid
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost})
}; };
switch (target.type) { switch (target.type) {
@@ -85,6 +84,7 @@ router.post('/', async(req, res) => {
} }
} }
/* create endpoint for outdial */ /* create endpoint for outdial */
const ms = getFreeswitch(); const ms = getFreeswitch();
if (!ms) throw new Error('no available Freeswitch for outbound call creation'); if (!ms) throw new Error('no available Freeswitch for outbound call creation');
@@ -136,7 +136,7 @@ router.post('/', async(req, res) => {
} }
else if (!app.notifier) { else if (!app.notifier) {
logger.debug('creating null call status hook'); logger.debug('creating null call status hook');
app.notifier = {request: () => {}, close: () => {}}; app.notifier = {request: () => {}};
} }
/* now launch the outdial */ /* now launch the outdial */
@@ -197,10 +197,9 @@ router.post('/', async(req, res) => {
}); });
cs.exec(req); cs.exec(req);
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')}); res.status(201).json({sid: cs.callSid});
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')}, sipLogger.info(`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
}, },
cbProvisional: (prov) => { cbProvisional: (prov) => {
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing; const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;

View File

@@ -2,7 +2,7 @@ const router = require('express').Router();
const CallInfo = require('../../session/call-info'); const CallInfo = require('../../session/call-info');
const {CallDirection} = require('../../utils/constants'); const {CallDirection} = require('../../utils/constants');
const SmsSession = require('../../session/sms-call-session'); const SmsSession = require('../../session/sms-call-session');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../../utils/normalize-jambones');
const makeTask = require('../../tasks/make_task'); const makeTask = require('../../tasks/make_task');
router.post('/:sid', async(req, res) => { router.post('/:sid', async(req, res) => {

View File

@@ -4,7 +4,7 @@ const WsRequestor = require('../../utils/ws-requestor');
const CallInfo = require('../../session/call-info'); const CallInfo = require('../../session/call-info');
const {CallDirection} = require('../../utils/constants'); const {CallDirection} = require('../../utils/constants');
const SmsSession = require('../../session/sms-call-session'); const SmsSession = require('../../session/sms-call-session');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../../utils/normalize-jambones');
const {TaskPreconditions} = require('../../utils/constants'); const {TaskPreconditions} = require('../../utils/constants');
const makeTask = require('../../tasks/make_task'); const makeTask = require('../../tasks/make_task');
@@ -34,7 +34,6 @@ router.post('/:partner', async(req, res) => {
carrier: req.params.partner, carrier: req.params.partner,
messageSid: app.messageSid, messageSid: app.messageSid,
accountSid: app.accountSid, accountSid: app.accountSid,
serviceProviderSid: account.service_provider_sid,
applicationSid: app.applicationSid, applicationSid: app.applicationSid,
from: req.body.from, from: req.body.from,
to: req.body.to, to: req.body.to,

View File

@@ -1,12 +1,11 @@
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const {CallDirection, AllowedSipRecVerbs} = require('./utils/constants'); const {CallDirection} = require('./utils/constants');
const {parseSiprecPayload} = require('./utils/siprec-utils');
const CallInfo = require('./session/call-info'); const CallInfo = require('./session/call-info');
const HttpRequestor = require('./utils/http-requestor'); const HttpRequestor = require('./utils/http-requestor');
const WsRequestor = require('./utils/ws-requestor'); const WsRequestor = require('./utils/ws-requestor');
const makeTask = require('./tasks/make_task'); const makeTask = require('./tasks/make_task');
const parseUri = require('drachtio-srf').parseUri; const parseUri = require('drachtio-srf').parseUri;
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('./utils/normalize-jambones');
const dbUtils = require('./utils/db-utils'); const dbUtils = require('./utils/db-utils');
const RootSpan = require('./utils/call-tracer'); const RootSpan = require('./utils/call-tracer');
const listTaskNames = require('./utils/summarize-tasks'); const listTaskNames = require('./utils/summarize-tasks');
@@ -19,26 +18,16 @@ module.exports = function(srf, logger) {
lookupAppByRealm, lookupAppByRealm,
lookupAppByTeamsTenant lookupAppByTeamsTenant
} = srf.locals.dbHelpers; } = srf.locals.dbHelpers;
const {
writeAlerts,
AlertType
} = srf.locals;
const {lookupAccountDetails} = dbUtils(logger, srf); const {lookupAccountDetails} = dbUtils(logger, srf);
function initLocals(req, res, next) { function initLocals(req, res, next) {
const callId = req.get('Call-ID');
logger.info({
callId,
callingNumber: req.callingNumber,
calledNumber: req.calledNumber
}, 'new incoming call');
if (!req.has('X-Account-Sid')) { if (!req.has('X-Account-Sid')) {
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header'); logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
return res.send(500); return res.send(500);
} }
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4(); const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
const account_sid = req.get('X-Account-Sid'); const account_sid = req.get('X-Account-Sid');
req.locals = {callSid, account_sid, callId}; req.locals = {callSid, account_sid};
if (req.has('X-Application-Sid')) { if (req.has('X-Application-Sid')) {
const application_sid = req.get('X-Application-Sid'); const application_sid = req.get('X-Application-Sid');
logger.debug(`got application from X-Application-Sid header: ${application_sid}`); logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
@@ -51,7 +40,7 @@ module.exports = function(srf, logger) {
} }
function createRootSpan(req, res, next) { function createRootSpan(req, res, next) {
const {callId, callSid, account_sid} = req.locals; const {callSid, account_sid} = req.locals;
const rootSpan = new RootSpan('incoming-call', req); const rootSpan = new RootSpan('incoming-call', req);
const traceId = rootSpan.traceId; const traceId = rootSpan.traceId;
@@ -59,7 +48,7 @@ module.exports = function(srf, logger) {
...req.locals, ...req.locals,
traceId, traceId,
logger: logger.child({ logger: logger.child({
callId, callId: req.get('Call-ID'),
callSid, callSid,
accountSid: account_sid, accountSid: account_sid,
callingNumber: req.callingNumber, callingNumber: req.callingNumber,
@@ -84,35 +73,6 @@ module.exports = function(srf, logger) {
next(); next();
} }
const handleSipRec = async(req, res, next) => {
if (Array.isArray(req.payload) && req.payload.length > 1) {
const {callId, logger} = req.locals;
logger.debug({payload: req.payload}, 'handling siprec call');
try {
const sdp = req.payload
.find((p) => p.type === 'application/sdp')
.content;
const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger);
req.locals.calledNumber = metadata.caller.number;
req.locals.callingNumber = metadata.callee.number;
req.locals = {
...req.locals,
siprec: {
metadata,
sdp1,
sdp2
}
};
logger.info({callId, metadata, sdp}, 'successfully parsed SIPREC payload');
} catch (err) {
logger.info({callId}, 'Error parsing multipart payload');
return res.send(503);
}
}
next();
};
/** /**
* retrieve account information for the incoming call * retrieve account information for the incoming call
*/ */
@@ -122,7 +82,6 @@ module.exports = function(srf, logger) {
const {span} = rootSpan.startChildSpan('lookupAccountDetails'); const {span} = rootSpan.startChildSpan('lookupAccountDetails');
try { try {
req.locals.accountInfo = await lookupAccountDetails(account_sid); req.locals.accountInfo = await lookupAccountDetails(account_sid);
req.locals.service_provider_sid = req.locals.accountInfo?.account?.service_provider_sid;
span.end(); span.end();
if (!req.locals.accountInfo.account.is_active) { if (!req.locals.accountInfo.account.is_active) {
logger.info(`Account is inactive or suspended ${account_sid}`); logger.info(`Account is inactive or suspended ${account_sid}`);
@@ -142,10 +101,7 @@ module.exports = function(srf, logger) {
* Within the system, we deal with E.164 numbers _without_ the leading '+ * Within the system, we deal with E.164 numbers _without_ the leading '+
*/ */
function normalizeNumbers(req, res, next) { function normalizeNumbers(req, res, next) {
const {logger, siprec} = req.locals; const logger = req.locals.logger;
if (siprec) return next();
Object.assign(req.locals, { Object.assign(req.locals, {
calledNumber: req.calledNumber, calledNumber: req.calledNumber,
callingNumber: req.callingNumber callingNumber: req.callingNumber
@@ -166,7 +122,8 @@ module.exports = function(srf, logger) {
* Given the dialed DID/phone number, retrieve the application to invoke * Given the dialed DID/phone number, retrieve the application to invoke
*/ */
async function retrieveApplication(req, res, next) { async function retrieveApplication(req, res, next) {
const {logger, accountInfo, account_sid, rootSpan} = req.locals; const logger = req.locals.logger;
const {accountInfo, account_sid, rootSpan} = req.locals;
const {span} = rootSpan.startChildSpan('lookupApplication'); const {span} = rootSpan.startChildSpan('lookupApplication');
try { try {
let app; let app;
@@ -229,32 +186,29 @@ module.exports = function(srf, logger) {
* create a requestor that we will use for all http requests we make during the call. * 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). * also create a notifier for call status events (if not needed, its a no-op).
*/ */
/* allow for caching data - when caching treat retrieved data as immutable */
const app2 = process.env.JAMBONES_MYSQL_REFRESH_TTL ? JSON.parse(JSON.stringify(app)) : app;
if ('WS' === app.call_hook?.method || if ('WS' === app.call_hook?.method ||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) { app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
app2.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ; app.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
app2.notifier = app.requestor; app.notifier = app.requestor;
app2.call_hook.method = 'WS'; app.call_hook.method = 'WS';
} }
else { else {
app2.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret); app.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
if (app.call_status_hook) app2.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook, if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
accountInfo.account.webhook_secret); accountInfo.account.webhook_secret);
else app2.notifier = {request: () => {}}; else app.notifier = {request: () => {}};
} }
req.locals.application = app2; req.locals.application = app;
const obj = Object.assign({}, app);
delete obj.requestor;
delete obj.notifier;
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook const {call_hook, call_status_hook, ...appInfo} = obj; // mask sensitive data like user/pass on webhook
// eslint-disable-next-line no-unused-vars logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
const {requestor, notifier, ...loggable} = appInfo;
logger.info({app: loggable}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
req.locals.callInfo = new CallInfo({ req.locals.callInfo = new CallInfo({
req, req,
app: app2, app,
direction: CallDirection.Inbound, direction: CallDirection.Inbound,
traceId: rootSpan.traceId traceId: rootSpan.traceId
}); });
@@ -271,71 +225,46 @@ module.exports = function(srf, logger) {
*/ */
async function invokeWebCallback(req, res, next) { async function invokeWebCallback(req, res, next) {
const logger = req.locals.logger; const logger = req.locals.logger;
const {rootSpan, siprec, application:app} = req.locals; const {rootSpan, application:app} = req.locals;
let span; let span;
try { try {
if (app.tasks && !process.env.JAMBONES_MYSQL_REFRESH_TTL) { if (app.tasks) {
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata)); app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
if (0 === app.tasks.length) throw new Error('no application provided'); if (0 === app.tasks.length) throw new Error('no application provided');
return next(); return next();
} }
/* retrieve the application to execute for this inbound call */ /* retrieve the application to execute for this inbound call */
let json; const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
if (app.app_json) { req.locals.callInfo, {
json = JSON.parse(app.app_json); defaults: {
} else { synthesizer: {
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {}, vendor: app.speech_synthesis_vendor,
req.locals.callInfo, language: app.speech_synthesis_language,
{ service_provider_sid: req.locals.service_provider_sid }, voice: app.speech_synthesis_voice
{ },
defaults: { recognizer: {
synthesizer: { vendor: app.speech_recognizer_vendor,
vendor: app.speech_synthesis_vendor, language: app.speech_recognizer_language
language: app.speech_synthesis_language,
voice: app.speech_synthesis_voice
},
recognizer: {
vendor: app.speech_recognizer_vendor,
language: app.speech_recognizer_language
}
} }
}); }
logger.debug({ params }, 'sending initial webhook'); });
const obj = rootSpan.startChildSpan('performAppWebhook'); logger.debug({params}, 'sending initial webhook');
span = obj.span; const obj = rootSpan.startChildSpan('performAppWebhook');
const b3 = rootSpan.getTracingPropagation(); span = obj.span;
const httpHeaders = b3 && { b3 }; const b3 = rootSpan.getTracingPropagation();
json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders); const httpHeaders = b3 && {b3};
} const json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata)); app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
span?.setAttributes({ span.setAttributes({
'http.statusCode': 200, 'http.statusCode': 200,
'app.tasks': listTaskNames(app.tasks) 'app.tasks': listTaskNames(app.tasks)
}); });
span?.end(); span.end();
if (0 === app.tasks.length) throw new Error('no application provided'); if (0 === app.tasks.length) throw new Error('no application provided');
if (siprec) {
const tasks = app.tasks.filter((t) => AllowedSipRecVerbs.includes(t.name));
if (0 === tasks.length) {
logger.info({tasks: app.tasks}, 'no valid verbs in app found for an incoming siprec call');
throw new Error('invalid verbs for incoming siprec call');
}
if (tasks.length < app.tasks.length) {
logger.info('removing verbs that are not allowed for incoming siprec call');
app.tasks = tasks;
}
}
next(); next();
} catch (err) { } catch (err) {
span?.setAttributes({webhookStatus: err.statusCode}); span?.setAttributes({webhookStatus: err.statusCode});
span?.end(); span?.end();
writeAlerts({
account_sid: req.locals.account_sid,
target_sid: req.locals.callSid,
alert_type: AlertType.INVALID_APP_PAYLOAD,
message: `${err?.message}`.trim()
}).catch((err) => this.logger.info({err}, 'Error generating alert for parsing application'));
logger.info({err}, `Error retrieving or parsing application: ${err?.message}`); logger.info({err}, `Error retrieving or parsing application: ${err?.message}`);
res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}}); res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}});
app.requestor.close(); app.requestor.close();
@@ -345,7 +274,6 @@ module.exports = function(srf, logger) {
return { return {
initLocals, initLocals,
createRootSpan, createRootSpan,
handleSipRec,
getAccountDetails, getAccountDetails,
normalizeNumbers, normalizeNumbers,
retrieveApplication, retrieveApplication,

View File

@@ -1,6 +1,6 @@
const {CallDirection, CallStatus} = require('../utils/constants'); const {CallDirection, CallStatus} = require('../utils/constants');
const parseUri = require('drachtio-srf').parseUri; const parseUri = require('drachtio-srf').parseUri;
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
/** /**
* @classdesc Represents the common information for all calls * @classdesc Represents the common information for all calls
* that is provided in call status webhooks * that is provided in call status webhooks
@@ -11,7 +11,6 @@ class CallInfo {
let srf; let srf;
this.direction = opts.direction; this.direction = opts.direction;
this.traceId = opts.traceId; this.traceId = opts.traceId;
this.callTerminationBy = undefined;
if (opts.req) { if (opts.req) {
const u = opts.req.getParsedHeader('from'); const u = opts.req.getParsedHeader('from');
const uri = parseUri(u.uri); const uri = parseUri(u.uri);
@@ -120,7 +119,7 @@ class CallInfo {
applicationSid: this.applicationSid, applicationSid: this.applicationSid,
fsSipAddress: this.localSipAddress fsSipAddress: this.localSipAddress
}; };
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName', 'callTerminationBy'].forEach((prop) => { ['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
if (this[prop]) obj[prop] = this[prop]; if (this[prop]) obj[prop] = this[prop];
}); });
if (typeof this.duration === 'number') obj.duration = this.duration; if (typeof this.duration === 'number') obj.duration = this.duration;

View File

@@ -1,19 +1,11 @@
const Emitter = require('events'); const Emitter = require('events');
const fs = require('fs'); const fs = require('fs');
const { const {CallDirection, TaskPreconditions, CallStatus, TaskName, KillReason} = require('../utils/constants');
CallDirection,
TaskPreconditions,
CallStatus,
TaskName,
KillReason,
RecordState,
AllowedSipRecVerbs
} = require('../utils/constants');
const moment = require('moment'); const moment = require('moment');
const assert = require('assert'); const assert = require('assert');
const sessionTracker = require('./session-tracker'); const sessionTracker = require('./session-tracker');
const makeTask = require('../tasks/make_task'); const makeTask = require('../tasks/make_task');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const listTaskNames = require('../utils/summarize-tasks'); const listTaskNames = require('../utils/summarize-tasks');
const HttpRequestor = require('../utils/http-requestor'); const HttpRequestor = require('../utils/http-requestor');
const WsRequestor = require('../utils/ws-requestor'); const WsRequestor = require('../utils/ws-requestor');
@@ -62,9 +54,6 @@ class CallSession extends Emitter {
assert(rootSpan); assert(rootSpan);
this._recordState = RecordState.RecordingOff;
this._notifyEvents = false;
this.tmpFiles = new Set(); this.tmpFiles = new Set();
if (!this.isSmsCallSession) { if (!this.isSmsCallSession) {
@@ -74,26 +63,12 @@ class CallSession extends Emitter {
if (!this.isConfirmCallSession && !this.isSmsCallSession && !this.isAdultingCallSession) { if (!this.isConfirmCallSession && !this.isSmsCallSession && !this.isAdultingCallSession) {
sessionTracker.add(this.callSid, this); sessionTracker.add(this.callSid, this);
const {startAmd, stopAmd} = require('../utils/amd-utils')(logger);
this.startAmd = startAmd;
this.stopAmd = stopAmd;
} }
this._pool = srf.locals.dbHelpers.pool; this._pool = srf.locals.dbHelpers.pool;
const handover = (newRequestor) => {
this.logger.info(`handover to new base url ${newRequestor.url}`);
this.requestor.removeAllListeners();
this.application.requestor = newRequestor;
this.requestor.on('command', this._onCommand.bind(this));
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this));
};
this.requestor.on('command', this._onCommand.bind(this)); this.requestor.on('command', this._onCommand.bind(this));
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this)); this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this));
} }
/** /**
@@ -110,14 +85,6 @@ class CallSession extends Emitter {
return this.callInfo.direction; return this.callInfo.direction;
} }
get applicationSid() {
return this.callInfo.applicationSid;
}
get callStatus() {
return this.callInfo.callStatus;
}
/** /**
* SIP call-id for the call * SIP call-id for the call
*/ */
@@ -244,13 +211,6 @@ class CallSession extends Emitter {
return this.constructor.name === 'ConfirmCallSession'; return this.constructor.name === 'ConfirmCallSession';
} }
/**
* returns true if this session is a SipRecCallSession
*/
get isSipRecCallSession() {
return this.constructor.name === 'SipRecCallSession';
}
/** /**
* returns true if this session is a SmsCallSession * returns true if this session is a SmsCallSession
*/ */
@@ -270,259 +230,19 @@ class CallSession extends Emitter {
return this.backgroundGatherTask; return this.backgroundGatherTask;
} }
get isListenEnabled() {
return this.backgroundListenTask;
}
get b3() { get b3() {
return this.rootSpan?.getTracingPropagation(); return this.rootSpan?.getTracingPropagation();
} }
get recordState() { return this._recordState; }
get notifyEvents() { return this._notifyEvents; }
set notifyEvents(notify) { this._notifyEvents = !!notify; }
set globalSttHints({hints, hintsBoost}) {
this._globalSttHints = {hints, hintsBoost};
}
get hasGlobalSttHints() {
const {hints = []} = this._globalSttHints || {};
return hints.length > 0;
}
get globalSttHints() {
return this._globalSttHints;
}
set altLanguages(langs) {
this._globalAltLanguages = langs;
}
get hasAltLanguages() {
return Array.isArray(this._globalAltLanguages);
}
get altLanguages() {
return this._globalAltLanguages;
}
set globalSttPunctuation(punctuate) {
this._globalSttPunctuation = punctuate;
}
get globalSttPunctuation() {
return this._globalSttPunctuation;
}
hasGlobalSttPunctuation() {
return this._globalSttPunctuation !== undefined;
}
async notifyRecordOptions(opts) {
const {action} = opts;
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
/* if we have not answered yet, just save the details for later */
if (!this.dlg) {
if (action === 'startCallRecording') {
this.recordOptions = opts;
return true;
}
return false;
}
/* check validity of request */
if (action == 'startCallRecording' && this.recordState !== RecordState.RecordingOff) {
this.logger.info({recordState: this.recordState},
'CallSession:notifyRecordOptions: recording is already started, ignoring request');
return false;
}
if (action == 'stopCallRecording' && this.recordState === RecordState.RecordingOff) {
this.logger.info({recordState: this.recordState},
'CallSession:notifyRecordOptions: recording is already stopped, ignoring request');
return false;
}
if (action == 'pauseCallRecording' && this.recordState !== RecordState.RecordingOn) {
this.logger.info({recordState: this.recordState},
'CallSession:notifyRecordOptions: cannot pause recording, ignoring request ');
return false;
}
if (action == 'resumeCallRecording' && this.recordState !== RecordState.RecordingPaused) {
this.logger.info({recordState: this.recordState},
'CallSession:notifyRecordOptions: cannot resume recording, ignoring request ');
return false;
}
this.recordOptions = opts;
switch (action) {
case 'startCallRecording':
return await this.startRecording();
case 'stopCallRecording':
return await this.stopRecording();
case 'pauseCallRecording':
return await this.pauseRecording();
case 'resumeCallRecording':
return await this.resumeRecording();
default:
throw new Error(`invalid record action ${action}`);
}
}
async startRecording() {
const {recordingID, siprecServerURL} = this.recordOptions;
assert(this.dlg);
this.logger.debug(`CallSession:startRecording - sending to ${siprecServerURL}`);
try {
const res = await this.dlg.request({
method: 'INFO',
headers: {
'X-Reason': 'startCallRecording',
'X-Srs-Url': siprecServerURL,
'X-Srs-Recording-ID': recordingID,
'X-Call-Sid': this.callSid,
'X-Account-Sid': this.accountSid,
'X-Application-Sid': this.applicationSid,
}
});
if (res.status === 200) {
this._recordState = RecordState.RecordingOn;
return true;
}
this.logger.info(`CallSession:startRecording - ${res.status} failure sending to ${siprecServerURL}`);
return false;
} catch (err) {
this.logger.info({err}, `CallSession:startRecording - failure sending to ${siprecServerURL}`);
return false;
}
}
async stopRecording() {
assert(this.dlg);
this.logger.debug('CallSession:stopRecording');
try {
const res = await this.dlg.request({
method: 'INFO',
headers: {
'X-Reason': 'stopCallRecording',
}
});
if (res.status === 200) {
this._recordState = RecordState.RecordingOff;
return true;
}
this.logger.info(`CallSession:stopRecording - ${res.status} failure`);
return false;
} catch (err) {
this.logger.info({err}, 'CallSession:startRecording - failure sending');
return false;
}
}
async pauseRecording() {
assert(this.dlg);
this.logger.debug('CallSession:pauseRecording');
try {
const res = await this.dlg.request({
method: 'INFO',
headers: {
'X-Reason': 'pauseCallRecording',
}
});
if (res.status === 200) {
this._recordState = RecordState.RecordingPaused;
return true;
}
this.logger.info(`CallSession:pauseRecording - ${res.status} failure`);
return false;
} catch (err) {
this.logger.info({err}, 'CallSession:pauseRecording - failure sending');
return false;
}
}
async resumeRecording() {
assert(this.dlg);
this.logger.debug('CallSession:resumeRecording');
try {
const res = await this.dlg.request({
method: 'INFO',
headers: {
'X-Reason': 'resumeCallRecording',
}
});
if (res.status === 200) {
this._recordState = RecordState.RecordingOn;
return true;
}
this.logger.info(`CallSession:resumeRecording - ${res.status} failure`);
return false;
} catch (err) {
this.logger.info({err}, 'CallSession:resumeRecording - failure sending');
return false;
}
}
async startBackgroundListen(opts) {
if (this.isListenEnabled) {
this.logger.info('CallSession:startBackgroundListen - listen is already enabled, ignoring request');
return;
}
try {
this.logger.debug({opts}, 'CallSession:startBackgroundListen');
const t = normalizeJambones(this.logger, [opts]);
this.backgroundListenTask = makeTask(this.logger, t[0]);
const resources = await this._evaluatePreconditions(this.backgroundListenTask);
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundListenTask.summary}`);
this.backgroundListenTask.span = span;
this.backgroundListenTask.ctx = ctx;
this.backgroundListenTask.exec(this, resources)
.then(() => {
this.logger.info('CallSession:startBackgroundListen: listen completed');
this.backgroundListenTask && this.backgroundListenTask.removeAllListeners();
this.backgroundListenTask && this.backgroundListenTask.span.end();
this.backgroundListenTask = null;
return;
})
.catch((err) => {
this.logger.info({err}, 'CallSession:startBackgroundListen: listen threw error');
this.backgroundListenTask && this.backgroundListenTask.removeAllListeners();
this.backgroundListenTask && this.backgroundListenTask.span.end();
this.backgroundListenTask = null;
});
} catch (err) {
this.logger.info({err, opts}, 'CallSession:startBackgroundListen - Error creating listen task');
}
}
async stopBackgroundListen() {
try {
if (this.backgroundListenTask) {
this.backgroundListenTask.removeAllListeners();
this.backgroundListenTask.kill().catch(() => {});
}
} catch (err) {
this.logger.info({err}, 'CallSession:stopBackgroundListen - Error stopping listen task');
}
this.backgroundListenTask = null;
}
async enableBotMode(gather, autoEnable) { async enableBotMode(gather, autoEnable) {
try { try {
if (this.backgroundGatherTask) {
this.logger.info('CallSession:enableBotMode - bot mode currently enabled, ignoring request to start again');
return;
}
const t = normalizeJambones(this.logger, [gather]); const t = normalizeJambones(this.logger, [gather]);
this.backgroundGatherTask = makeTask(this.logger, t[0]); this.backgroundGatherTask = makeTask(this.logger, t[0]);
this._bargeInEnabled = true;
this.backgroundGatherTask this.backgroundGatherTask
.once('dtmf', this._clearTasks.bind(this, this.backgroundGatherTask)) .once('dtmf', this._clearTasks.bind(this))
.once('vad', this._clearTasks.bind(this, this.backgroundGatherTask)) .once('vad', this._clearTasks.bind(this))
.once('transcription', this._clearTasks.bind(this, this.backgroundGatherTask)) .once('transcription', this._clearTasks.bind(this))
.once('timeout', this._clearTasks.bind(this, this.backgroundGatherTask)); .once('timeout', this._clearTasks.bind(this));
this.logger.info({gather}, 'CallSession:enableBotMode - starting background gather'); this.logger.info({gather}, 'CallSession:enableBotMode - starting background gather');
const resources = await this._evaluatePreconditions(this.backgroundGatherTask); const resources = await this._evaluatePreconditions(this.backgroundGatherTask);
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`); const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`);
@@ -534,7 +254,7 @@ class CallSession extends Emitter {
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners(); this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
this.backgroundGatherTask && this.backgroundGatherTask.span.end(); this.backgroundGatherTask && this.backgroundGatherTask.span.end();
this.backgroundGatherTask = null; this.backgroundGatherTask = null;
if (autoEnable && !this.callGone && !this._stopping && this._bargeInEnabled) { if (autoEnable && !this.callGone && !this._stopping) {
this.logger.info('CallSession:enableBotMode: restarting background gather'); this.logger.info('CallSession:enableBotMode: restarting background gather');
setImmediate(() => this.enableBotMode(gather, true)); setImmediate(() => this.enableBotMode(gather, true));
} }
@@ -551,7 +271,6 @@ class CallSession extends Emitter {
} }
} }
disableBotMode() { disableBotMode() {
this._bargeInEnabled = false;
if (this.backgroundGatherTask) { if (this.backgroundGatherTask) {
try { try {
this.backgroundGatherTask.removeAllListeners(); this.backgroundGatherTask.removeAllListeners();
@@ -619,11 +338,7 @@ class CallSession extends Emitter {
return { return {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key, api_key: credential.api_key,
region: credential.region, region: credential.region
use_custom_stt: credential.use_custom_stt,
custom_stt_endpoint: credential.custom_stt_endpoint,
use_custom_tts: credential.use_custom_tts,
custom_tts_endpoint: credential.custom_tts_endpoint
}; };
} }
else if ('wellsaid' === vendor) { else if ('wellsaid' === vendor) {
@@ -632,42 +347,6 @@ class CallSession extends Emitter {
api_key: credential.api_key api_key: credential.api_key
}; };
} }
else if ('nuance' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
client_id: credential.client_id,
secret: credential.secret
};
}
else if ('deepgram' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key
};
}
else if ('soniox' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key
};
}
else if ('ibm' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
tts_api_key: credential.tts_api_key,
tts_region: credential.tts_region,
stt_api_key: credential.stt_api_key,
stt_region: credential.stt_region
};
}
else if (vendor.startsWith('custom:')) {
return {
speech_credential_sid: credential.speech_credential_sid,
auth_token: credential.auth_token,
custom_stt_url: credential.custom_stt_url,
custom_tts_url: credential.custom_tts_url
};
}
} }
else { else {
writeAlerts({ writeAlerts({
@@ -692,22 +371,15 @@ class CallSession extends Emitter {
const stackNum = this.stackIdx; const stackNum = this.stackIdx;
const task = this.tasks.shift(); const task = this.tasks.shift();
this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`);
this._notifyTaskStatus(task, {event: 'starting'});
try { try {
const resources = await this._evaluatePreconditions(task); const resources = await this._evaluatePreconditions(task);
let skip = false;
this.currentTask = task; this.currentTask = task;
if (TaskName.Gather === task.name && this.isBotModeEnabled) { if (TaskName.Gather === task.name && this.isBotModeEnabled) {
if (this.backgroundGatherTask.updateTaskInProgress(task)) { const timeout = task.timeout;
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
skip = true; this.backgroundGatherTask.updateTimeout(timeout);
}
else {
this.logger.info('CallSession:exec disabling bot mode to start gather with new options');
this.disableBotMode();
}
} }
if (!skip) { else {
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`); const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
task.span = span; task.span = span;
task.ctx = ctx; task.ctx = ctx;
@@ -716,7 +388,6 @@ class CallSession extends Emitter {
} }
this.currentTask = null; this.currentTask = null;
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
this._notifyTaskStatus(task, {event: 'finished'});
} catch (err) { } catch (err) {
task.span?.end(); task.span?.end();
this.currentTask = null; this.currentTask = null;
@@ -729,7 +400,7 @@ class CallSession extends Emitter {
} }
} }
if (0 === this.tasks.length && this.requestor instanceof WsRequestor && !this.callGone) { if (0 === this.tasks.length && this.hasStableDialog && this.requestor instanceof WsRequestor) {
let span; let span;
try { try {
const {span} = this.rootSpan.startChildSpan('waiting for commands'); const {span} = this.rootSpan.startChildSpan('waiting for commands');
@@ -740,7 +411,7 @@ class CallSession extends Emitter {
'async.request.command': command 'async.request.command': command
}); });
span.end(); span.end();
if (this.callGone) break; if (!this.hasStableDialog || this.callGone) break;
} catch (err) { } catch (err) {
span.end(); span.end();
this.logger.info(err, 'CallSession:exec - error waiting for new commands'); this.logger.info(err, 'CallSession:exec - error waiting for new commands');
@@ -756,12 +427,12 @@ class CallSession extends Emitter {
this._onTasksDone(); this._onTasksDone();
this._clearResources(); this._clearResources();
if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid); if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid);
} }
trackTmpFile(path) { trackTmpFile(path) {
// TODO: don't add if its already in the list (should we make it a set?) // TODO: don't add if its already in the list (should we make it a set?)
this.logger.debug(`adding tmp file to track ${path}`);
this.tmpFiles.add(path); this.tmpFiles.add(path);
} }
@@ -1053,9 +724,6 @@ class CallSession extends Emitter {
const res = await this._lccSipRequest(opts, callSid); const res = await this._lccSipRequest(opts, callSid);
return {status: res.status, reason: res.reason}; return {status: res.status, reason: res.reason};
} }
else if (opts.record) {
await this.notifyRecordOptions(opts.record);
}
// whisper may be the only thing we are asked to do, or it may that // 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.. // we are doing a whisper after having muted, paused reccording etc..
@@ -1073,20 +741,6 @@ class CallSession extends Emitter {
this.logger.debug('CallSession:replaceApplication - ignoring because call is gone'); this.logger.debug('CallSession:replaceApplication - ignoring because call is gone');
return; return;
} }
if (this.isSipRecCallSession) {
const pruned = tasks.filter((t) => AllowedSipRecVerbs.includes(t.name));
if (0 === pruned.length) {
this.logger.info({tasks},
'CallSession:replaceApplication - only config, transcribe and/or listen allowed on an incoming siprec call');
return;
}
if (pruned.length < tasks.length) {
this.logger.info(
'CallSession:replaceApplication - removing verbs that are not allowed for incoming siprec call');
tasks = pruned;
}
}
this.tasks = tasks; this.tasks = tasks;
this.taskIdx = 0; this.taskIdx = 0;
this.stackIdx++; this.stackIdx++;
@@ -1098,32 +752,14 @@ class CallSession extends Emitter {
} }
} }
kill(onBackgroundGatherBargein = false) { kill() {
if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)'); if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)');
else this.logger.info('CallSession:kill'); else this.logger.info('CallSession:kill');
if (this.currentTask) { if (this.currentTask) {
this.currentTask.kill(this); this.currentTask.kill(this);
this.currentTask = null; this.currentTask = null;
} }
if (onBackgroundGatherBargein) { this.tasks = [];
/* search for a config with bargein disabled */
while (this.tasks.length) {
const t = this.tasks[0];
if (t.name === TaskName.Config && t.bargeIn?.enable === false) {
/* found it, clear to that point and remove the disable
because we likely already received a partial transcription
and we don't want to kill the background gather before we
get the full transcription.
*/
delete t.bargeIn.enable;
this._bargeInEnabled = false;
this.logger.info('CallSession:kill - found bargein disabled in the stack, clearing to that point');
break;
}
this.tasks.shift();
}
}
else this.tasks = [];
this.taskIdx = 0; this.taskIdx = 0;
} }
@@ -1136,14 +772,14 @@ class CallSession extends Emitter {
_injectTasks(newTasks) { _injectTasks(newTasks) {
const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather); const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather);
const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather; const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather;
/*
this.logger.debug({ this.logger.debug({
currentTaskList: listTaskNames(this.tasks), currentTaskList: listTaskNames(this.tasks),
newContent: listTaskNames(newTasks), newContent: listTaskNames(newTasks),
currentlyExecutingGather, currentlyExecutingGather,
gatherPos gatherPos
}, 'CallSession:_injectTasks - starting'); }, 'CallSession:_injectTasks - starting');
*/
const killGather = () => { const killGather = () => {
this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content'); this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content');
this.currentTask.kill(this); this.currentTask.kill(this);
@@ -1152,11 +788,10 @@ class CallSession extends Emitter {
if (-1 === gatherPos) { if (-1 === gatherPos) {
/* no gather in the stack simply append tasks */ /* no gather in the stack simply append tasks */
this.tasks.push(...newTasks); this.tasks.push(...newTasks);
/*
this.logger.debug({ this.logger.debug({
updatedTaskList: listTaskNames(this.tasks) updatedTaskList: listTaskNames(this.tasks)
}, 'CallSession:_injectTasks - completed (simple append)'); }, 'CallSession:_injectTasks - completed (simple append)');
*/
/* we do need to kill the current gather if we are executing one */ /* we do need to kill the current gather if we are executing one */
if (currentlyExecutingGather) killGather(); if (currentlyExecutingGather) killGather();
return; return;
@@ -1184,10 +819,12 @@ class CallSession extends Emitter {
this.replaceApplication(t); this.replaceApplication(t);
} }
else if (process.env.JAMBONES_INJECT_CONTENT) { else if (process.env.JAMBONES_INJECT_CONTENT) {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks (injecting content)');
this._injectTasks(t); this._injectTasks(t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
else { else {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
this.tasks.push(...t); this.tasks.push(...t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
@@ -1231,7 +868,7 @@ class CallSession extends Emitter {
this.logger.info(`CallSession:_onCommand - invalid command ${command}`); this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
} }
if (this.wakeupResolver) { if (this.wakeupResolver) {
//this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..'); this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..');
this.wakeupResolver(resolution); this.wakeupResolver(resolution);
this.wakeupResolver = null; this.wakeupResolver = null;
} }
@@ -1271,36 +908,28 @@ class CallSession extends Emitter {
* @param {Task} task - task to be executed * @param {Task} task - task to be executed
*/ */
async _evalEndpointPrecondition(task) { async _evalEndpointPrecondition(task) {
this.logger.debug('CallSession:_evalEndpointPrecondition');
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`); if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
if (this.ep) { if (this.ep) {
const resources = {ep: this.ep}; if (task.earlyMedia === true || this.dlg) return this.ep;
if (task.earlyMedia === true || this.dlg) {
return {
...resources,
...(this.isSipRecCallSession && {ep2: this.ep2})
};
}
// we are going from an early media connection to answer // we are going from an early media connection to answer
await this.propagateAnswer(); await this.propagateAnswer();
return { return this.ep;
...resources,
...(this.isSipRecCallSession && {ep2: this.ep2})
};
} }
// need to allocate an endpoint // need to allocate an endpoint
try { try {
if (!this.ms) this.ms = this.getMS(); if (!this.ms) this.ms = this.getMS();
const ep = await this.ms.createEndpoint({ const ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
headers: {
'X-Jambones-Call-ID': this.callId,
},
remoteSdp: this.req.body
});
//ep.cs = this; //ep.cs = this;
this.ep = ep; this.ep = ep;
ep.set({
hangup_after_bridge: false,
park_after_bridge: true
}).catch((err) => this.logger.error({err}, 'Error setting park_after_bridge'));
this.logger.debug(`allocated endpoint ${ep.uuid}`); this.logger.debug(`allocated endpoint ${ep.uuid}`);
this.ep.on('destroy', () => { this.ep.on('destroy', () => {
@@ -1310,7 +939,7 @@ class CallSession extends Emitter {
if (this.direction === CallDirection.Inbound) { if (this.direction === CallDirection.Inbound) {
if (task.earlyMedia && !this.req.finalResponseSent) { if (task.earlyMedia && !this.req.finalResponseSent) {
this.res.send(183, {body: ep.local.sdp}); this.res.send(183, {body: ep.local.sdp});
return {ep}; return ep;
} }
this.logger.debug('propogating answer'); this.logger.debug('propogating answer');
await this.propagateAnswer(); await this.propagateAnswer();
@@ -1319,11 +948,10 @@ class CallSession extends Emitter {
// outbound call TODO // outbound call TODO
} }
return {ep}; return ep;
} catch (err) { } catch (err) {
if (err === CALLER_CANCELLED_ERR_MSG) { if (err === CALLER_CANCELLED_ERR_MSG) {
this.logger.error(err, 'caller canceled quickly before we could respond, ending call'); this.logger.error(err, 'caller canceled quickly before we could respond, ending call');
this.callInfo.callTerminationBy = 'caller';
this._notifyCallStatusChange({ this._notifyCallStatusChange({
callStatus: CallStatus.NoAnswer, callStatus: CallStatus.NoAnswer,
sipStatus: 487, sipStatus: 487,
@@ -1345,7 +973,7 @@ class CallSession extends Emitter {
_evalStableCallPrecondition(task) { _evalStableCallPrecondition(task) {
if (this.callGone) throw new Error(`${BADPRECONDITIONS}: call gone`); if (this.callGone) throw new Error(`${BADPRECONDITIONS}: call gone`);
if (!this.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered`); if (!this.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered`);
return {dlg: this.dlg}; return this.dlg;
} }
/** /**
@@ -1373,6 +1001,7 @@ class CallSession extends Emitter {
return; return;
} }
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp}); this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
await this.ep.set('hangup_after_bridge', false);
await this.dlg.modify(this.ep.local.sdp); await this.dlg.modify(this.ep.local.sdp);
this.logger.debug('CallSession:replaceEndpoint completed'); this.logger.debug('CallSession:replaceEndpoint completed');
@@ -1383,7 +1012,7 @@ class CallSession extends Emitter {
* Hang up the call and free the media endpoint * Hang up the call and free the media endpoint
*/ */
_clearResources() { _clearResources() {
for (const resource of [this.dlg, this.ep, this.ep2]) { for (const resource of [this.dlg, this.ep]) {
if (resource && resource.connected) resource.destroy(); if (resource && resource.connected) resource.destroy();
} }
this.dlg = null; this.dlg = null;
@@ -1400,7 +1029,6 @@ class CallSession extends Emitter {
} }
this.tmpFiles.clear(); this.tmpFiles.clear();
this.requestor && this.requestor.close(); this.requestor && this.requestor.close();
this.notifier && this.notifier.close();
this.rootSpan && this.rootSpan.end(); this.rootSpan && this.rootSpan.end();
} }
@@ -1438,8 +1066,7 @@ class CallSession extends Emitter {
this.dlg = await this.srf.createUAS(this.req, this.res, { this.dlg = await this.srf.createUAS(this.req, this.res, {
headers: { headers: {
'X-Trace-ID': this.req.locals.traceId, 'X-Trace-ID': this.req.locals.traceId,
'X-Call-Sid': this.req.locals.callSid, 'X-Call-Sid': this.req.locals.callSid
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
}, },
localSdp: this.ep.local.sdp localSdp: this.ep.local.sdp
}); });
@@ -1449,9 +1076,6 @@ class CallSession extends Emitter {
this.dlg.callSid = this.callSid; this.dlg.callSid = this.callSid;
this.emit('callStatusChange', {sipStatus: 200, sipReason: 'OK', callStatus: CallStatus.InProgress}); this.emit('callStatusChange', {sipStatus: 200, sipReason: 'OK', callStatus: CallStatus.InProgress});
if (this.recordOptions && this.recordState === RecordState.RecordingOff) {
this.startRecording();
}
this.dlg.on('modify', this._onReinvite.bind(this)); this.dlg.on('modify', this._onReinvite.bind(this));
this.dlg.on('refer', this._onRefer.bind(this)); this.dlg.on('refer', this._onRefer.bind(this));
@@ -1462,15 +1086,9 @@ class CallSession extends Emitter {
async _onReinvite(req, res) { async _onReinvite(req, res) {
try { try {
if (this.ep) { if (this.ep) {
if (this.isSipRecCallSession) { const newSdp = await this.ep.modify(req.body);
this.logger.info('handling reINVITE for siprec call'); res.send(200, {body: newSdp});
res.send(200, {body: this.ep.local.sdp}); this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
}
else {
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) { else if (this.currentTask && this.currentTask.name === TaskName.Dial) {
this.logger.info('handling reINVITE after media has been released'); this.logger.info('handling reINVITE after media has been released');
@@ -1516,6 +1134,7 @@ class CallSession extends Emitter {
} }
if (!this.ep) { if (!this.ep) {
this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body}); this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
await this.ep.set('hangup_after_bridge', false);
} }
return {ms: this.ms, ep: this.ep}; return {ms: this.ms, ep: this.ep};
} }
@@ -1598,8 +1217,7 @@ class CallSession extends Emitter {
headers: { headers: {
'Refer-To': referTo, 'Refer-To': referTo,
'Referred-By': `sip:${this.srf.locals.localSipAddress}`, 'Referred-By': `sip:${this.srf.locals.localSipAddress}`,
'X-Retain-Call-Sid': this.callSid, 'X-Retain-Call-Sid': this.callSid
'X-Account-Sid': this.accountSid
} }
}); });
if ([200, 202].includes(res.status)) { if ([200, 202].includes(res.status)) {
@@ -1637,11 +1255,10 @@ class CallSession extends Emitter {
dlg.connected = false; dlg.connected = false;
dlg.destroy = origDestroy; dlg.destroy = origDestroy;
const duration = moment().diff(this.dlg.connectTime, 'seconds'); const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.callInfo.callTerminationBy = 'jambonz';
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('CallSession: call terminated by jambonz'); this.logger.debug('CallSession: call terminated by jambones');
this.rootSpan.setAttributes({'call.termination': 'hangup by jambonz'}); this.rootSpan.setAttributes({'call.termination': 'hangup by jambonz'});
origDestroy().catch((err) => this.logger.info({err}, 'CallSession - error destroying dialog')); origDestroy();
if (this.wakeupResolver) { if (this.wakeupResolver) {
this.wakeupResolver({reason: 'session ended'}); this.wakeupResolver({reason: 'session ended'});
this.wakeupResolver = null; this.wakeupResolver = null;
@@ -1688,7 +1305,7 @@ class CallSession extends Emitter {
* @param {number} sipStatus - current sip status * @param {number} sipStatus - current sip status
* @param {number} [duration] - duration of a completed call, in seconds * @param {number} [duration] - duration of a completed call, in seconds
*/ */
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) { _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
if (this.callMoved) return; if (this.callMoved) return;
/* race condition: we hang up at the same time as the caller */ /* race condition: we hang up at the same time as the caller */
@@ -1708,7 +1325,7 @@ class CallSession extends Emitter {
try { try {
const b3 = this.b3; const b3 = this.b3;
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
await this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON(), httpHeaders); this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON(), httpHeaders);
span.end(); span.end();
} catch (err) { } catch (err) {
span.end(); span.end();
@@ -1721,25 +1338,6 @@ class CallSession extends Emitter {
.catch((err) => this.logger.error(err, 'redis error')); .catch((err) => this.logger.error(err, 'redis error'));
} }
/**
* notifyTaskError - only used when websocket connection is used instead of webhooks
*/
_notifyTaskError(obj) {
if (this.requestor instanceof WsRequestor) {
this.requestor.request('jambonz:error', '/error', obj)
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskError - Error sending'));
}
}
_notifyTaskStatus(task, evt) {
if (this.notifyEvents && this.requestor instanceof WsRequestor) {
const obj = {...evt, id: task.id, name: task.name};
this.requestor.request('verb:status', '/status', obj)
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskStatus - Error sending'));
}
}
_awaitCommandsOrHangup() { _awaitCommandsOrHangup() {
assert(!this.wakeupResolver); assert(!this.wakeupResolver);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -1748,12 +1346,11 @@ class CallSession extends Emitter {
}); });
} }
_clearTasks(backgroundGather, evt) { _clearTasks(evt) {
if (this.requestor instanceof WsRequestor && !backgroundGather.cleared) { if (this.requestor instanceof WsRequestor) {
this.logger.info({evt}, 'CallSession:_clearTasks on event from background gather'); this.logger.info({evt}, 'CallSession:_clearTasks on event from background gather');
try { try {
backgroundGather.cleared = true; this.kill();
this.kill(true);
} catch (err) {} } catch (err) {}
} }
} }

View File

@@ -34,7 +34,6 @@ class InboundCallSession extends CallSession {
_onCancel() { _onCancel() {
this.rootSpan.setAttributes({'call.termination': 'caller abandoned'}); this.rootSpan.setAttributes({'call.termination': 'caller abandoned'});
this.callInfo.callTerminationBy = 'caller';
this._notifyCallStatusChange({ this._notifyCallStatusChange({
callStatus: CallStatus.NoAnswer, callStatus: CallStatus.NoAnswer,
sipStatus: 487, sipStatus: 487,
@@ -70,7 +69,6 @@ class InboundCallSession extends CallSession {
assert(this.dlg.connectTime); assert(this.dlg.connectTime);
const duration = moment().diff(this.dlg.connectTime, 'seconds'); const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.rootSpan.setAttributes({'call.termination': 'hangup by caller'}); this.rootSpan.setAttributes({'call.termination': 'hangup by caller'});
this.callInfo.callTerminationBy = 'caller';
this.emit('callStatusChange', { this.emit('callStatusChange', {
callStatus: CallStatus.Completed, callStatus: CallStatus.Completed,
duration duration

View File

@@ -44,7 +44,6 @@ class RestCallSession extends CallSession {
* This is invoked when the called party hangs up, in order to calculate the call duration. * This is invoked when the called party hangs up, in order to calculate the call duration.
*/ */
_callerHungup() { _callerHungup() {
this.callInfo.callTerminationBy = 'caller';
const duration = moment().diff(this.dlg.connectTime, 'seconds'); 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('RestCallSession: called party hung up'); this.logger.debug('RestCallSession: called party hung up');

View File

@@ -1,59 +0,0 @@
const InboundCallSession = require('./inbound-call-session');
const {createSipRecPayload} = require('../utils/siprec-utils');
const {CallStatus} = require('../utils/constants');
/**
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is
* established for an inbound SIPREC call.
* @extends InboundCallSession
*/
class SipRecCallSession extends InboundCallSession {
constructor(req, res) {
super(req, res);
const {sdp1, sdp2, metadata} = req.locals.siprec;
this.sdp1 = sdp1;
this.sdp2 = sdp2;
this.metadata = metadata;
}
async answerSipRecCall() {
try {
this.ms = this.getMS();
let remoteSdp = this.sdp1.replace(/sendonly/, 'sendrecv');
this.ep = await this.ms.createEndpoint({remoteSdp});
//this.logger.debug({remoteSdp, localSdp: this.ep.local.sdp}, 'SipRecCallSession - allocated first endpoint');
remoteSdp = this.sdp2.replace(/sendonly/, 'sendrecv');
this.ep2 = await this.ms.createEndpoint({remoteSdp});
//this.logger.debug({remoteSdp, localSdp: this.ep2.local.sdp}, 'SipRecCallSession - allocated second endpoint');
await this.ep.bridge(this.ep2);
const combinedSdp = await createSipRecPayload(this.ep.local.sdp, this.ep2.local.sdp, this.logger);
/*
this.logger.debug({
combinedSdp
}, 'SipRecCallSession:_answerSipRecCall - created SIPREC payload');
*/
this.dlg = await this.srf.createUAS(this.req, this.res, {
headers: {
'Content-Type': 'application/sdp',
'X-Trace-ID': this.req.locals.traceId,
'X-Call-Sid': this.req.locals.callSid,
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
},
localSdp: combinedSdp
});
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.dlg.on('modify', this._onReinvite.bind(this));
this.dlg.on('refer', this._onRefer.bind(this));
} catch (err) {
this.logger.error({err}, 'SipRecCallSession:_answerSipRecCall error:');
if (this.res && !this.res.finalResponseSent) this.res.send(500);
this._callReleased();
}
}
}
module.exports = SipRecCallSession;

View File

@@ -2,7 +2,7 @@ const Task = require('./task');
const Emitter = require('events'); const Emitter = require('events');
const ConfirmCallSession = require('../session/confirm-call-session'); const ConfirmCallSession = require('../session/confirm-call-session');
const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants'); const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const bent = require('bent'); const bent = require('bent');
const assert = require('assert'); const assert = require('assert');
@@ -72,7 +72,7 @@ class Conference extends Task {
get shouldRecord() { return this.record.path; } get shouldRecord() { return this.record.path; }
get isRecording() { return this.recordingInProgress; } get isRecording() { return this.recordingInProgress; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
const dlg = cs.dlg; const dlg = cs.dlg;
@@ -108,10 +108,6 @@ class Conference extends Task {
async kill(cs) { async kill(cs) {
super.kill(cs); super.kill(cs);
this.logger.info(`Conference:kill ${this.confName}`); this.logger.info(`Conference:kill ${this.confName}`);
if (this._playSession) {
this._playSession.kill();
this._playSession = null;
}
this.emitter.emit('kill'); this.emitter.emit('kill');
await this._doFinalMemberCheck(cs); await this._doFinalMemberCheck(cs);
if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ; if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ;
@@ -431,19 +427,13 @@ class Conference extends Task {
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant')); .catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
} }
if (wait_hook) {
if (this.wait_hook)
delete this.wait_hook.url;
this.wait_hook = {url: wait_hook};
}
if (hookOnly && this._playSession) { if (hookOnly && this._playSession) {
this._playSession.kill(); this._playSession.kill();
this._playSession = null; this._playSession = null;
} }
if (this.wait_hook?.url && this.conf_hold_status === 'hold') { if (wait_hook && this.conf_hold_status === 'hold') {
const {dlg} = cs; const {dlg} = cs;
this._doWaitHookWhileOnHold(cs, dlg, this.wait_hook); this._doWaitHookWhileOnHold(cs, dlg, wait_hook);
} }
else if (this.conf_hold_status !== 'hold' && this._playSession) { else if (this.conf_hold_status !== 'hold' && this._playSession) {
this._playSession.kill(); this._playSession.kill();
@@ -454,9 +444,7 @@ class Conference extends Task {
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) { async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
do { do {
try { try {
let tasks = []; const tasks = await this._playHook(cs, dlg, wait_hook);
if (wait_hook.url)
tasks = await this._playHook(cs, dlg, wait_hook.url);
if (0 === tasks.length) break; if (0 === tasks.length) break;
} catch (err) { } catch (err) {
if (!this.killed) { if (!this.killed) {
@@ -465,7 +453,7 @@ class Conference extends Task {
this._playSession = null; this._playSession = null;
break; break;
} }
} while (!this.killed && this.conf_hold_status === 'hold'); } while (!this.killed && this.conf_hold_status !== 'hold');
} }
/** /**
@@ -553,9 +541,6 @@ class Conference extends Task {
} }
this.logger.debug(`Conference:_playHook: executing ${tasks.length} tasks`); this.logger.debug(`Conference:_playHook: executing ${tasks.length} tasks`);
/* we might have been killed while off fetching waitHook */
if (this.killed) return [];
if (tasks.length > 0) { if (tasks.length > 0) {
this._playSession = new ConfirmCallSession({ this._playSession = new ConfirmCallSession({
logger: this.logger, logger: this.logger,
@@ -566,8 +551,7 @@ class Conference extends Task {
accountInfo: cs.accountInfo, accountInfo: cs.accountInfo,
memberId: this.memberId, memberId: this.memberId,
confName: this.confName, confName: this.confName,
tasks, tasks
rootSpan: cs.rootSpan
}); });
await this._playSession.exec(); await this._playSession.exec();
this._playSession = null; this._playSession = null;
@@ -583,10 +567,6 @@ class Conference extends Task {
*/ */
_kicked(cs, dlg) { _kicked(cs, dlg) {
this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`); this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`);
if (this._playSession) {
this._playSession.kill();
this._playSession = null;
}
this.replaceEndpointAndEnd(cs); this.replaceEndpointAndEnd(cs);
} }

View File

@@ -4,18 +4,14 @@ const {TaskName, TaskPreconditions} = require('../utils/constants');
class TaskConfig extends Task { class TaskConfig extends Task {
constructor(logger, opts) { constructor(logger, opts) {
super(logger, opts); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
[ [
'synthesizer', 'synthesizer',
'recognizer', 'recognizer',
'bargeIn', 'bargeIn'
'record',
'listen'
].forEach((k) => this[k] = this.data[k] || {}); ].forEach((k) => this[k] = this.data[k] || {});
if ('notifyEvents' in this.data) {
this.notifyEvents = !!this.data.notifyEvents;
}
if (this.bargeIn.enable) { if (this.bargeIn.enable) {
this.gatherOpts = { this.gatherOpts = {
verb: 'gather', verb: 'gather',
@@ -31,17 +27,14 @@ class TaskConfig extends Task {
}); });
} }
if (this.bargeIn.sticky) this.autoEnable = true; if (this.bargeIn.sticky) this.autoEnable = true;
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ? this.preconditions = this.bargeIn.enable ? TaskPreconditions.Endpoint : TaskPreconditions.None;
TaskPreconditions.Endpoint :
TaskPreconditions.None;
} }
get name() { return TaskName.Config; } get name() { return TaskName.Config; }
get hasSynthesizer() { return Object.keys(this.synthesizer).length; } get hasSynthesizer() { return Object.keys(this.synthesizer).length; }
get hasRecognizer() { return Object.keys(this.recognizer).length; } get hasRecognizer() { return Object.keys(this.recognizer).length; }
get hasRecording() { return Object.keys(this.record).length; }
get hasListen() { return Object.keys(this.listen).length; }
get summary() { get summary() {
const phrase = []; const phrase = [];
@@ -56,36 +49,12 @@ class TaskConfig extends Task {
const s = `{${v},${l}}`; const s = `{${v},${l}}`;
phrase.push(`set recognizer${s}`); phrase.push(`set recognizer${s}`);
} }
if (this.hasRecording) phrase.push(this.record.action); return `${this.name}{${phrase.join(',')}`;
if (this.hasListen) {
phrase.push(this.listen.enable ? `listen ${this.listen.url}` : 'stop listen');
}
if (this.data.amd) phrase.push('enable amd');
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
return `${this.name}{${phrase.join(',')}`;
} }
async exec(cs, {ep} = {}) { async exec(cs) {
await super.exec(cs); await super.exec(cs);
if (this.notifyEvents) {
this.logger.debug(`turning event notification ${this.notifyEvents ? 'on' : 'off'}`);
cs.notifyEvents = !!this.data.notifyEvents;
}
if (this.data.amd) {
this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs));
try {
this.ep = ep;
this.startAmd(cs, ep, this, this.data.amd);
} catch (err) {
this.logger.info({err}, 'Config:exec - Error calling startAmd');
}
}
if (this.hasSynthesizer) { if (this.hasSynthesizer) {
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default' cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
? this.synthesizer.vendor ? this.synthesizer.vendor
@@ -105,32 +74,10 @@ class TaskConfig extends Task {
cs.speechRecognizerLanguage = this.recognizer.language !== 'default' cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
? this.recognizer.language ? this.recognizer.language
: cs.speechRecognizerLanguage; : cs.speechRecognizerLanguage;
cs.isContinuousAsr = typeof this.recognizer.asrTimeout === 'number' ? true : false; this.logger.info({recognizer: this.recognizer}, 'Config: updated recognizer');
if (cs.isContinuousAsr) {
cs.asrTimeout = this.recognizer.asrTimeout;
cs.asrDtmfTerminationDigit = this.recognizer.asrDtmfTerminationDigit;
}
if (Array.isArray(this.recognizer.hints)) {
const obj = {hints: this.recognizer.hints};
if (typeof this.recognizer.hintsBoost === 'number') {
obj.hintsBoost = this.recognizer.hintsBoost;
}
cs.globalSttHints = obj;
}
if (Array.isArray(this.recognizer.altLanguages)) {
this.logger.info({altLanguages: this.recognizer.altLanguages}, 'Config: updated altLanguages');
cs.altLanguages = this.recognizer.altLanguages;
}
if ('punctuation' in this.recognizer) {
cs.globalSttPunctuation = this.recognizer.punctuation;
}
this.logger.info({
recognizer: this.recognizer,
isContinuousAsr: cs.isContinuousAsr
}, 'Config: updated recognizer');
} }
if ('enable' in this.bargeIn) { if ('enable' in this.bargeIn) {
if (this.bargeIn.enable === true && this.gatherOpts) { if (this.gatherOpts) {
this.gatherOpts.recognizer = this.hasRecognizer ? this.gatherOpts.recognizer = this.hasRecognizer ?
this.recognizer : this.recognizer :
{ {
@@ -140,43 +87,15 @@ class TaskConfig extends Task {
this.logger.info({opts: this.gatherOpts}, 'Config: enabling bargeIn'); this.logger.info({opts: this.gatherOpts}, 'Config: enabling bargeIn');
cs.enableBotMode(this.gatherOpts, this.autoEnable); cs.enableBotMode(this.gatherOpts, this.autoEnable);
} }
else if (this.bargeIn.enable === false) { else {
this.logger.info('Config: disabling bargeIn'); this.logger.info('Config: disabling bargeIn');
cs.disableBotMode(); cs.disableBotMode();
} }
} }
if (this.record.action) {
try {
await cs.notifyRecordOptions(this.record);
} catch (err) {
this.logger.info({err}, 'Config: error starting recording');
}
}
if (this.hasListen) {
const {enable, ...opts} = this.listen;
if (enable) {
this.logger.debug({opts}, 'Config: enabling listen');
cs.startBackgroundListen({verb: 'listen', ...opts});
} else {
this.logger.info('Config: disabling listen');
cs.stopBackgroundListen();
}
}
} }
async kill(cs) { async kill(cs) {
super.kill(cs); super.kill(cs);
//if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
}
_onAmdEvent(cs, evt) {
this.logger.info({evt}, 'Config:_onAmdEvent');
const {actionHook} = this.data.amd;
this.performHook(cs, actionHook, evt)
.catch((err) => {
this.logger.error({err}, 'Config:_onAmdEvent - error calling actionHook');
});
} }
} }

View File

@@ -23,7 +23,7 @@ class TaskDequeue extends Task {
get name() { return TaskName.Dequeue; } get name() { return TaskName.Dequeue; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
this.queueName = `queue:${cs.accountSid}:${this.queueName}`; this.queueName = `queue:${cs.accountSid}:${this.queueName}`;

View File

@@ -134,10 +134,7 @@ class TaskDial extends Task {
get name() { return TaskName.Dial; } get name() { return TaskName.Dial; }
get canReleaseMedia() { get canReleaseMedia() {
return !process.env.ANCHOR_MEDIA_ALWAYS && return !process.env.ANCHOR_MEDIA_ALWAYS && !this.listenTask && !this.transcribeTask;
!this.listenTask &&
!this.transcribeTask &&
!this.startAmd;
} }
get summary() { get summary() {
@@ -161,11 +158,6 @@ class TaskDial extends Task {
async exec(cs) { async exec(cs) {
await super.exec(cs); await super.exec(cs);
try { try {
if (this.data.amd) {
this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs));
}
if (cs.direction === CallDirection.Inbound) { if (cs.direction === CallDirection.Inbound) {
await this._initializeInbound(cs); await this._initializeInbound(cs);
} }
@@ -189,11 +181,6 @@ class TaskDial extends Task {
async kill(cs, reason) { async kill(cs, reason) {
super.kill(cs); super.kill(cs);
try {
if (this.ep && this.ep.amd) this.stopAmd(this.ep, this);
} catch (err) {
this.logger.error({err}, 'DialTask:kill - error stopping answering machine detectin');
}
if (this.dialMusic && this.epOther) { if (this.dialMusic && this.epOther) {
this.epOther.api('uuid_break', this.epOther.uuid) this.epOther.api('uuid_break', this.epOther.uuid)
.catch((err) => this.logger.info(err, 'Error killing dialMusic')); .catch((err) => this.logger.info(err, 'Error killing dialMusic'));
@@ -216,14 +203,8 @@ class TaskDial extends Task {
this.sd = null; this.sd = null;
} }
if (this.callSid) sessionTracker.remove(this.callSid); if (this.callSid) sessionTracker.remove(this.callSid);
if (this.listenTask) { if (this.listenTask) await this.listenTask.kill(cs);
await this.listenTask.kill(cs); if (this.transcribeTask) await this.transcribeTask.kill(cs);
this.listenTask = null;
}
if (this.transcribeTask) {
await this.transcribeTask.kill(cs);
this.transcribeTask = null;
}
this.notifyTaskDone(); this.notifyTaskDone();
} }
@@ -245,10 +226,10 @@ class TaskDial extends Task {
this.logger.debug('Dial:whisper executing tasks'); this.logger.debug('Dial:whisper executing tasks');
while (tasks.length && !cs.callGone) { while (tasks.length && !cs.callGone) {
const task = tasks.shift(); const task = tasks.shift();
const {span, ctx} = this.startChildSpan(`whisper:${task.summary}`); const {span, ctx} = this.startChildSpan(`whisper:${this.sayTask.summary}`);
task.span = span; task.span = span;
task.ctx = ctx; task.ctx = ctx;
await task.exec(cs, callSid === this.callSid ? {ep: this.ep} : {ep: this.epOther}); await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
span.end(); span.end();
} }
this.logger.debug('Dial:whisper tasks complete'); this.logger.debug('Dial:whisper tasks complete');
@@ -376,8 +357,9 @@ class TaskDial extends Task {
} }
async _initializeInbound(cs) { async _initializeInbound(cs) {
const {ep} = await cs._evalEndpointPrecondition(this); const ep = await cs._evalEndpointPrecondition(this);
this.epOther = ep; this.epOther = ep;
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
/* send outbound legs back to the same SBC (to support static IP feature) */ /* 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}`;
@@ -400,19 +382,15 @@ class TaskDial extends Task {
let fqdn; let fqdn;
if (!sbcAddress) throw new Error('no SBC found for outbound call'); if (!sbcAddress) throw new Error('no SBC found for outbound call');
this.headers = {
'X-Account-Sid': cs.accountSid,
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
...(req && req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
// Put headers at the end to make sure opt.headers override all default behavior.
...this.headers
};
const opts = { const opts = {
headers: this.headers, headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers,
proxy: `sip:${sbcAddress}`, proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber callingNumber: this.callerId || req.callingNumber
}; };
opts.headers = {
...opts.headers,
'X-Account-Sid': cs.accountSid
};
const t = this.target.find((t) => t.type === 'teams'); const t = this.target.find((t) => t.type === 'teams');
if (t) { if (t) {
@@ -426,11 +404,6 @@ class TaskDial extends Task {
this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`); this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`);
this.timerRing = null; this.timerRing = null;
this._killOutdials(); this._killOutdials();
this.result = {
dialCallStatus: CallStatus.NoAnswer,
dialSipStatus: 487
};
this.kill(cs);
}, this.timeout * 1000); }, this.timeout * 1000);
this.span.setAttributes({'dial.target': JSON.stringify(this.target)}); this.span.setAttributes({'dial.target': JSON.stringify(this.target)});
@@ -593,7 +566,6 @@ class TaskDial extends Task {
* - save the dialog and endpoint * - save the dialog and endpoint
* - clock the start time of the call, * - clock the start time of the call,
* - start a max call length timer (optionally) * - start a max call length timer (optionally)
* - start answering machine detection (optionally)
* - launch any nested tasks * - launch any nested tasks
* - and establish a handler to clean up if the called party hangs up * - and establish a handler to clean up if the called party hangs up
*/ */
@@ -634,18 +606,11 @@ class TaskDial extends Task {
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg); if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg); if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep2: this.epOther, ep:this.ep}); if (this.transcribeTask) this.transcribeTask.exec(cs, this.epOther, this.ep);
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther}); if (this.listenTask) this.listenTask.exec(cs, this.epOther);
if (this.startAmd) {
try {
this.startAmd(cs, this.ep, this, this.data.amd);
} catch (err) {
this.logger.info({err}, 'Dial:_selectSingleDial - Error calling startAmd');
}
}
/* if we can release the media back to the SBC, do so now */ /* if we can release the media back to the SBC, do so now */
if (this.canReleaseMedia) setTimeout(this._releaseMedia.bind(this, cs, sd), 200); if (this.canReleaseMedia) this._releaseMedia(cs, sd);
} }
_bridgeEarlyMedia(sd) { _bridgeEarlyMedia(sd) {
@@ -689,15 +654,6 @@ class TaskDial extends Task {
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg'); this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
res.send(200, {body: sdp}); res.send(200, {body: sdp});
} }
_onAmdEvent(cs, evt) {
this.logger.info({evt}, 'Dial:_onAmdEvent');
const {actionHook} = this.data.amd;
this.performHook(cs, actionHook, evt)
.catch((err) => {
this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook');
});
}
} }
module.exports = TaskDial; module.exports = TaskDial;

View File

@@ -3,7 +3,7 @@ const {TaskName, TaskPreconditions} = require('../../utils/constants');
const Intent = require('./intent'); const Intent = require('./intent');
const DigitBuffer = require('./digit-buffer'); const DigitBuffer = require('./digit-buffer');
const Transcription = require('./transcription'); const Transcription = require('./transcription');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../../utils/normalize-jambones');
class Dialogflow extends Task { class Dialogflow extends Task {
constructor(logger, opts) { constructor(logger, opts) {
@@ -64,7 +64,7 @@ class Dialogflow extends Task {
get name() { return TaskName.Dialogflow; } get name() { return TaskName.Dialogflow; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
try { try {

View File

@@ -12,7 +12,7 @@ class TaskDtmf extends Task {
get name() { return TaskName.Dtmf; } get name() { return TaskName.Dtmf; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
try { try {

View File

@@ -1,7 +1,7 @@
const Task = require('./task'); const Task = require('./task');
const Emitter = require('events'); const Emitter = require('events');
const ConfirmCallSession = require('../session/confirm-call-session'); const ConfirmCallSession = require('../session/confirm-call-session');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants'); const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants');
const bent = require('bent'); const bent = require('bent');
@@ -37,7 +37,7 @@ class TaskEnqueue extends Task {
get name() { return TaskName.Enqueue; } get name() { return TaskName.Enqueue; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
const dlg = cs.dlg; const dlg = cs.dlg;
this.queueName = `queue:${cs.accountSid}:${this.queueName}`; this.queueName = `queue:${cs.accountSid}:${this.queueName}`;
@@ -349,8 +349,7 @@ class TaskEnqueue extends Task {
ep: cs.ep, ep: cs.ep,
callInfo: cs.callInfo, callInfo: cs.callInfo,
accountInfo: cs.accountInfo, accountInfo: cs.accountInfo,
tasks: tasksToRun, tasks: tasksToRun
rootSpan: cs.rootSpan
}); });
await this._playSession.exec(); await this._playSession.exec();
this._playSession = null; this._playSession = null;

View File

@@ -3,77 +3,68 @@ const {
TaskName, TaskName,
TaskPreconditions, TaskPreconditions,
GoogleTranscriptionEvents, GoogleTranscriptionEvents,
NuanceTranscriptionEvents,
AwsTranscriptionEvents, AwsTranscriptionEvents,
AzureTranscriptionEvents, AzureTranscriptionEvents
DeepgramTranscriptionEvents,
SonioxTranscriptionEvents,
IbmTranscriptionEvents,
NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('../utils/constants'); } = require('../utils/constants');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const assert = require('assert'); const assert = require('assert');
//const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7);
const compileTranscripts = (logger, evt, arr) => {
if (!Array.isArray(arr) || arr.length === 0) return;
let t = '';
for (const a of arr) {
t += ` ${a.alternatives[0].transcript}`;
}
t += ` ${evt.alternatives[0].transcript}`;
evt.alternatives[0].transcript = t.trim();
};
class TaskGather extends Task { class TaskGather extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
super(logger, opts); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
const {
setChannelVarsForStt,
normalizeTranscription,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
} = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription;
this.removeSpeechListeners = removeSpeechListeners;
this.compileSonioxTranscripts = compileSonioxTranscripts;
[ [
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
'speechTimeout', 'timeout', 'say', 'play' 'speechTimeout', 'timeout', 'say', 'play'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
/* when collecting dtmf, bargein on dtmf is true unless explicitly set to false */ /* when collecting dtmf, bargein on dtmf is true unless explicitly set to false */
if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true; if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true;
/* timeout of zero means no timeout */ /* timeout of zero means no timeout */
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000; this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0); this.interim = this.partialResultHook || this.bargein;
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true; this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount || 1; this.minBargeinWordCount = this.data.minBargeinWordCount || 0;
if (this.data.recognizer) { if (this.data.recognizer) {
const recognizer = this.data.recognizer; const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor; this.vendor = recognizer.vendor;
this.language = recognizer.language; this.language = recognizer.language;
this.hints = recognizer.hints || [];
this.hintsBoost = recognizer.hintsBoost;
this.profanityFilter = recognizer.profanityFilter;
this.punctuation = !!recognizer.punctuation;
this.enhancedModel = !!recognizer.enhancedModel;
this.model = recognizer.model || 'command_and_search';
this.words = !!recognizer.words;
this.singleUtterance = recognizer.singleUtterance || true;
this.diarization = !!recognizer.diarization;
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
this.interactionType = recognizer.interactionType || 'unspecified';
this.naicsCode = recognizer.naicsCode || 0;
this.altLanguages = recognizer.altLanguages || [];
/* let credentials be supplied in the recognizer object at runtime */ /* vad: if provided, we dont connect to recognizer until voice activity is detected */
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer); const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
this.vad = {enable, voiceMs, mode};
/* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */ /* aws options */
this.asrTimeout = typeof recognizer.asrTimeout === 'number' ? recognizer.asrTimeout * 1000 : 0; this.vocabularyName = recognizer.vocabularyName;
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit; this.vocabularyFilterName = recognizer.vocabularyFilterName;
this.isContinuousAsr = this.asrTimeout > 0; this.filterMethod = recognizer.filterMethod;
this.data.recognizer.hints = this.data.recognizer.hints || []; /* microsoft options */
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || []; this.outputFormat = recognizer.outputFormat || 'simple';
this.profanityOption = recognizer.profanityOption || 'raw';
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
} }
else this.data.recognizer = {hints: [], altLanguages: []};
this.digitBuffer = ''; this.digitBuffer = '';
this._earlyMedia = this.data.earlyMedia === true; this._earlyMedia = this.data.earlyMedia === true;
@@ -86,12 +77,6 @@ class TaskGather extends Task {
} }
if (!this.sayTask && !this.playTask) this.listenDuringPrompt = false; if (!this.sayTask && !this.playTask) this.listenDuringPrompt = false;
/* buffer speech for continuous asr */
this._bufferedTranscripts = [];
/* buffer for soniox transcripts */
this._sonioxTranscripts = [];
this.parentTask = parentTask; this.parentTask = parentTask;
} }
@@ -119,61 +104,15 @@ class TaskGather extends Task {
return s; return s;
} }
async exec(cs, {ep}) { async exec(cs, ep) {
this.logger.debug('Gather:exec'); this.logger.debug('Gather:exec');
await super.exec(cs); await super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
if (cs.hasGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints;
const setOfHints = new Set(this.data.recognizer.hints
.concat(hints)
.filter((h) => typeof h === 'string' && h.length > 0));
this.data.recognizer.hints = [...setOfHints];
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost;
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
'Gather:exec - applying global sttHints');
}
if (cs.hasAltLanguages) {
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'Gather:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
this.data.recognizer.punctuation = cs.globalSttPunctuation;
}
if (!this.isContinuousAsr && cs.isContinuousAsr) {
this.isContinuousAsr = true;
this.asrTimeout = cs.asrTimeout * 1000;
this.asrDtmfTerminationDigit = cs.asrDtmfTerminationDigit;
this.logger.debug({
asrTimeout: this.asrTimeout,
asrDtmfTerminationDigit: this.asrDtmfTerminationDigit
}, 'Gather:exec - enabling continuous ASR since it is turned on for the session');
}
const {JAMBONZ_GATHER_EARLY_HINTS_MATCH, JAMBONES_GATHER_EARLY_HINTS_MATCH} = process.env;
if ((JAMBONZ_GATHER_EARLY_HINTS_MATCH || JAMBONES_GATHER_EARLY_HINTS_MATCH) && this.needsStt &&
!this.isContinuousAsr &&
this.data.recognizer?.hints?.length > 0 && this.data.recognizer?.hints?.length <= 10) {
this.earlyHintsMatch = true;
this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled');
}
this.ep = ep; this.ep = ep;
if ('default' === this.vendor || !this.vendor) { if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
this.vendor = cs.speechRecognizerVendor; if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor; this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (this.needsStt && !this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
if (this.needsStt && !this.sttCredentials) { if (this.needsStt && !this.sttCredentials) {
const {writeAlerts, AlertType} = cs.srf.locals; 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 creds not supplied`);
@@ -182,45 +121,19 @@ class TaskGather extends Task {
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor vendor: this.vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
// Notify application that STT vender is wrong.
this.notifyError({ throw new Error(`no speech-to-text service credentials for ${this.vendor} have been configured`);
msg: 'ASR error',
details: `No speech-to-text service credentials for ${this.vendor} have been configured`
});
this.notifyTaskDone();
throw new Error(`No speech-to-text service credentials for ${this.vendor} have been configured`);
} }
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
/* get nuance access token */
const {client_id, secret} = this.sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
this.logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token};
}
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
/* get ibm access token */
const {stt_api_key, stt_region} = this.sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
}
const startListening = (cs, ep) => { const startListening = (cs, ep) => {
this._startTimer(); this._startTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
if (this.input.includes('speech') && !this.listenDuringPrompt) { if (this.input.includes('speech') && !this.listenDuringPrompt) {
this._initSpeech(cs, ep) this._initSpeech(cs, ep)
.then(() => { .then(() => {
if (this.killed) {
this.logger.info('Gather:exec - task was quickly killed so do not transcribe');
return;
}
this._startTranscribing(ep); this._startTranscribing(ep);
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
}) })
.catch((err) => { .catch(() => {});
this.logger.error({err}, 'error in initSpeech');
});
} }
}; };
@@ -229,49 +142,27 @@ class TaskGather extends Task {
const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`);
this.sayTask.span = span; this.sayTask.span = span;
this.sayTask.ctx = ctx; this.sayTask.ctx = ctx;
this.sayTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
this.sayTask.on('playDone', (err) => { this.sayTask.on('playDone', (err) => {
span.end(); span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing tts'); if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
this.logger.debug('Gather: nested say task completed'); this.logger.debug('Gather: nested say task completed');
if (!this.killed) { if (!this.killed) startListening(cs, ep);
startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
this.logger.debug('Gather:exec - starting transcription timers after say completes');
ep.startTranscriptionTimers((err) => {
if (err) this.logger.error({err}, 'Gather:exec - error starting transcription timers');
});
}
}
}); });
} }
else if (this.playTask) { else if (this.playTask) {
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
this.playTask.span = span; this.playTask.span = span;
this.playTask.ctx = ctx; this.playTask.ctx = ctx;
this.playTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
this.playTask.on('playDone', (err) => { this.playTask.on('playDone', (err) => {
span.end(); span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing url'); if (err) this.logger.error({err}, 'Gather:exec Error playing url');
this.logger.debug('Gather: nested play task completed'); this.logger.debug('Gather: nested play task completed');
if (!this.killed) { if (!this.killed) startListening(cs, ep);
startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
this.logger.debug('Gather:exec - starting transcription timers after play completes');
ep.startTranscriptionTimers((err) => {
if (err) this.logger.error({err}, 'Gather:exec - error starting transcription timers');
});
}
}
}); });
} }
else { else startListening(cs, ep);
if (this.killed) {
this.logger.info('Gather:exec - task was immediately killed so do not transcribe');
return;
}
startListening(cs, ep);
}
if (this.input.includes('speech') && this.listenDuringPrompt) { if (this.input.includes('speech') && this.listenDuringPrompt) {
await this._initSpeech(cs, ep); await this._initSpeech(cs, ep);
@@ -280,7 +171,7 @@ class TaskGather extends Task {
.catch(() => {/*already logged error */}); .catch(() => {/*already logged error */});
} }
if (this.input.includes('digits') || this.dtmfBargein || this.asrDtmfTerminationDigit) { if (this.input.includes('digits') || this.dtmfBargein) {
ep.on('dtmf', this._onDtmf.bind(this, cs, ep)); ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
} }
@@ -288,7 +179,14 @@ class TaskGather extends Task {
} catch (err) { } catch (err) {
this.logger.error(err, 'TaskGather:exec error'); this.logger.error(err, 'TaskGather:exec error');
} }
this.removeSpeechListeners(ep); 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) { kill(cs) {
@@ -296,18 +194,13 @@ class TaskGather extends Task {
this._killAudio(cs); this._killAudio(cs);
this.ep.removeAllListeners('dtmf'); this.ep.removeAllListeners('dtmf');
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearAsrTimer();
this.playTask?.span.end(); this.playTask?.span.end();
this.sayTask?.span.end(); this.sayTask?.span.end();
this._resolve('killed'); this._resolve('killed');
} }
updateTaskInProgress(opts) { updateTimeout(timeout) {
if (!this.needsStt && opts.input.includes('speech')) { this.logger.info(`TaskGather:updateTimeout - updating timeout to ${timeout}`);
this.logger.info('TaskGather:updateTaskInProgress - adding speech to a background gather');
return false; // this needs be handled by killing the background gather and starting a new one
}
const {timeout} = opts;
this.timeout = timeout; this.timeout = timeout;
this._startTimer(); this._startTimer();
} }
@@ -316,15 +209,12 @@ class TaskGather extends Task {
this.logger.debug(evt, 'TaskGather:_onDtmf'); this.logger.debug(evt, 'TaskGather:_onDtmf');
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
let resolved = false; let resolved = false;
if (this.dtmfBargein) { if (this.dtmfBargein) this._killAudio(cs);
this._killAudio(cs);
this.emit('dtmf', evt);
}
if (evt.dtmf === this.finishOnKey && this.input.includes('digits')) { if (evt.dtmf === this.finishOnKey && this.input.includes('digits')) {
resolved = true; resolved = true;
this._resolve('dtmf-terminator-key'); this._resolve('dtmf-terminator-key');
} }
else if (this.input.includes('digits')) { else {
this.digitBuffer += evt.dtmf; this.digitBuffer += evt.dtmf;
const len = this.digitBuffer.length; const len = this.digitBuffer.length;
if (len === this.numDigits || len === this.maxDigits) { if (len === this.numDigits || len === this.maxDigits) {
@@ -332,13 +222,6 @@ class TaskGather extends Task {
this._resolve('dtmf-num-digits'); this._resolve('dtmf-num-digits');
} }
} }
else if (this.isContinuousAsr && evt.dtmf === this.asrDtmfTerminationDigit) {
this.logger.info(`continuousAsr triggered with dtmf ${this.asrDtmfTerminationDigit}`);
this._clearAsrTimer();
this._clearTimer();
this._startFinalAsrTimer();
return;
}
if (!resolved && this.interDigitTimeout > 0 && this.digitBuffer.length >= this.minDigits) { if (!resolved && this.interDigitTimeout > 0 && this.digitBuffer.length >= this.minDigits) {
/* start interDigitTimer */ /* start interDigitTimer */
const ms = this.interDigitTimeout * 1000; const ms = this.interDigitTimeout * 1000;
@@ -348,101 +231,90 @@ class TaskGather extends Task {
} }
async _initSpeech(cs, ep) { async _initSpeech(cs, ep) {
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer); const opts = {};
switch (this.vendor) {
case 'google':
this.bugname = 'google_transcribe';
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));
break;
case 'aws': if (this.vad?.enable) {
case 'polly': opts.START_RECOGNIZING_ON_VAD = 1;
this.bugname = 'aws_transcribe'; if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); else opts.RECOGNIZER_VAD_VOICE_MS = 125;
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep)); if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
break;
case 'microsoft':
this.bugname = 'azure_transcribe';
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));
break;
case 'nuance':
this.bugname = 'nuance_transcribe';
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
opts.NUANCE_STALL_TIMERS = 1;
}
break;
case 'deepgram':
this.bugname = 'deepgram_transcribe';
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect, this._onDeepgramConnect.bind(this, cs, ep));
ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
this._onDeepGramConnectFailure.bind(this, cs, ep));
break;
case 'soniox':
this.bugname = 'soniox_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break;
case 'ibm':
this.bugname = 'ibm_transcribe';
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(IbmTranscriptionEvents.Connect, this._onIbmConnect.bind(this, cs, ep));
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onIbmConnectFailure.bind(this, cs, ep));
break;
case 'nvidia':
this.bugname = 'nvidia_transcribe';
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* I think nvidia has this (??) - stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
opts.NVIDIA_STALL_TIMERS = 1;
}
break;
default:
if (this.vendor.startsWith('custom:')) {
this.bugname = `${this.vendor}_transcribe`;
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onJambonzConnect.bind(this, cs, ep));
ep.addCustomEventListener(JambonzTranscriptionEvents.ConnectFailure,
this._onJambonzConnectFailure.bind(this, cs, ep));
break;
}
else {
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`});
this.notifyTaskDone();
throw new Error(`Invalid vendor ${this.vendor}`);
}
} }
/* common handler for all stt engine errors */ if ('google' === this.vendor) {
ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep)); if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
['singleUtterance', 'GOOGLE_SPEECH_SINGLE_UTTERANCE'],
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].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.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
if ('unspecified' !== this.interactionType) {
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
}
opts.GOOGLE_SPEECH_MODEL = this.model;
if (this.diarization && this.diarizationMinSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
}
if (this.diarization && this.diarizationMaxSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
}
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
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)) {
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
});
}
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.altLanguages && this.altLanguages.length > 0) {
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.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;
else if (this.timeout === 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = 120000; // lengthy
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));
}
await ep.set(opts) await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables')); .catch((err) => this.logger.info(err, 'Error setting channel variables'));
} }
@@ -451,14 +323,12 @@ class TaskGather extends Task {
this.logger.debug({ this.logger.debug({
vendor: this.vendor, vendor: this.vendor,
locale: this.language, locale: this.language,
interim: this.interim, interim: this.interim
bugname: this.bugname
}, 'Gather:_startTranscribing'); }, 'Gather:_startTranscribing');
ep.startTranscription({ ep.startTranscription({
vendor: this.vendor, vendor: this.vendor,
locale: this.language, locale: this.language,
interim: this.interim, interim: this.interim,
bugname: this.bugname,
}).catch((err) => { }).catch((err) => {
const {writeAlerts, AlertType} = this.cs.srf.locals; const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error'); this.logger.error(err, 'TaskGather:_startTranscribing error');
@@ -473,10 +343,14 @@ class TaskGather extends Task {
_startTimer() { _startTimer() {
if (0 === this.timeout) return; if (0 === this.timeout) return;
this._clearTimer(); if (this._timeoutTimer) {
clearTimeout(this._timeoutTimer);
this._timeoutTimer = null;
}
assert(!this._timeoutTimer);
this.logger.debug(`Gather:_startTimer: timeout ${this.timeout}`);
this._timeoutTimer = setTimeout(() => { this._timeoutTimer = setTimeout(() => {
if (this.isContinuousAsr) this._startAsrTimer(); this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
else this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
}, this.timeout); }, this.timeout);
} }
@@ -487,35 +361,6 @@ class TaskGather extends Task {
} }
} }
_startAsrTimer() {
assert(this.isContinuousAsr);
this._clearAsrTimer();
this._asrTimer = setTimeout(() => {
this.logger.debug('_startAsrTimer - asr timer went off');
this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
}, this.asrTimeout);
this.logger.debug(`_startAsrTimer: set for ${this.asrTimeout}ms`);
}
_clearAsrTimer() {
if (this._asrTimer) clearTimeout(this._asrTimer);
this._asrTimer = null;
}
_startFinalAsrTimer() {
this._clearFinalAsrTimer();
this._finalAsrTimer = setTimeout(() => {
this.logger.debug('_startFinalAsrTimer - final asr timer went off');
this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
}, 1000);
this.logger.debug('_startFinalAsrTimer: set for 1 second');
}
_clearFinalAsrTimer() {
if (this._finalAsrTimer) clearTimeout(this._finalAsrTimer);
this._finalAsrTimer = null;
}
_killAudio(cs) { _killAudio(cs) {
if (!this.sayTask && !this.playTask && this.bargein) { if (!this.sayTask && !this.playTask && this.bargein) {
if (this.ep?.connected && !this.playComplete) { if (this.ep?.connected && !this.playComplete) {
@@ -538,96 +383,53 @@ class TaskGather extends Task {
} }
} }
_onTranscription(cs, ep, evt, fsEvent) { _onTranscription(cs, ep, evt) {
// make sure this is not a transcript from answering machine detection if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
const bugname = fsEvent.getHeader('media-bugname'); if ('microsoft' === this.vendor) {
const finished = fsEvent.getHeader('transcription-session-finished'); const final = evt.RecognitionStatus === 'Success';
this.logger.debug({evt, bugname, finished}, `Gather:_onTranscription for vendor ${this.vendor}`); if (final) {
if (bugname && this.bugname !== bugname) return; // don't sort based on confidence: https://github.com/Azure-Samples/cognitive-services-speech-sdk/issues/1463
//const nbest = evt.NBest.sort((a, b) => b.Confidence - a.Confidence);
if (this.vendor === 'ibm') { const nbest = evt.NBest;
if (evt?.state === 'listening') return; const language_code = evt.PrimaryLanguage?.Language || this.language;
} evt = {
is_final: true,
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language); language_code,
alternatives: [
if (this.earlyHintsMatch && evt.is_final === false) { {
const transcript = evt.alternatives[0].transcript?.toLowerCase(); confidence: nbest[0].Confidence,
const hints = this.data.recognizer?.hints || []; transcript: nbest[0].Display
if (hints.find((h) => h.toLowerCase() === transcript)) { }
this.logger.debug({evt}, 'Gather:_onTranscription: early hint match'); ]
this._resolve('speech', evt); };
return;
}
}
/* count words for bargein feature */
const words = evt.alternatives[0]?.transcript.split(' ').length;
const bufferedWords = this._sonioxTranscripts.length +
this._bufferedTranscripts.reduce((count, e) => count + e.alternatives[0]?.transcript.split(' ').length, 0);
if (evt.is_final) {
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
if (finished === 'true' && ['microsoft', 'deepgram'].includes(this.vendor)) {
this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding');
}
else {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
}
return;
}
if (this.isContinuousAsr) {
/* append the transcript and start listening again for asrTimeout */
const t = evt.alternatives[0].transcript;
if (t) {
/* remove trailing punctuation */
if (/[,;:\.!\?]$/.test(t)) {
this.logger.debug('TaskGather:_onTranscription - removing trailing punctuation');
evt.alternatives[0].transcript = t.slice(0, -1);
}
else this.logger.debug({t}, 'TaskGather:_onTranscription - no trailing punctuation');
}
this.logger.info({evt}, 'TaskGather:_onTranscription - got transcript during continous asr');
this._bufferedTranscripts.push(evt);
this._clearTimer();
if (this._finalAsrTimer) {
this._clearFinalAsrTimer();
return this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
}
this._startAsrTimer();
/* some STT engines will keep listening after a final response, so no need to restart */
if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) this._startTranscribing(ep);
} }
else { else {
if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) { evt = {
this.logger.debug({evt, words, bufferedWords}, is_final: false,
'TaskGather:_onTranscription - final transcript but < min barge words'); alternatives: [
this._bufferedTranscripts.push(evt); {
this._startTranscribing(ep); transcript: evt.Text
return; }
} ]
else { };
if (this.vendor === 'soniox') {
/* compile transcripts into one */
this._sonioxTranscripts.push(evt.vendor.finalWords);
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
this._sonioxTranscripts = [];
}
this._resolve('speech', evt);
}
} }
} }
if (evt.is_final) {
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
return this._startTranscribing(ep);
}
this._resolve('speech', evt);
}
else { else {
/* google has a measure of stability: /* google has a measure of stability:
https://cloud.google.com/speech-to-text/docs/basics#streaming_responses https://cloud.google.com/speech-to-text/docs/basics#streaming_responses
others do not. others do not.
*/ */
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD; //const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
this._clearTimer();
this._startTimer(); if (this.bargein && /* isStableEnough && */
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) { evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) {
if (!this.playComplete) { if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech'); this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
this.emit('vad'); this.emit('vad');
@@ -637,16 +439,9 @@ class TaskGather extends Task {
if (this.partialResultHook) { if (this.partialResultHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt}, this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt},
this.cs.callInfo, httpHeaders)); this.cs.callInfo, httpHeaders));
} }
if (this.vendor === 'soniox') {
this._clearTimer();
if (evt.vendor.finalWords.length) {
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
this._sonioxTranscripts.push(evt.vendor.finalWords);
}
}
} }
} }
_onEndOfUtterance(cs, ep) { _onEndOfUtterance(cs, ep) {
@@ -655,93 +450,11 @@ class TaskGather extends Task {
this._killAudio(cs); this._killAudio(cs);
} }
if (!this.resolved && !this.killed && !this._bufferedTranscripts.length) { if (!this.resolved && !this.killed) {
this._startTranscribing(ep); this._startTranscribing(ep);
} }
} }
_onStartOfSpeech(cs, ep) {
this.logger.debug('TaskGather:_onStartOfSpeech');
if (this.bargein) {
this._killAudio(cs);
}
}
_onTranscriptionComplete(cs, ep) {
this.logger.debug('TaskGather:_onTranscriptionComplete');
}
_onDeepgramConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onDeepgramConnect');
}
_onJambonzConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onJambonzConnect');
}
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskGather:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
this.logger.info({evt}, 'TaskGather:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
_onDeepGramConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onDeepgramConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
vendor: 'deepgram',
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor deepgram: ${reason}`});
this.notifyTaskDone();
}
_onJambonzConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onJambonzConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
this.notifyTaskDone();
}
_onIbmConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onIbmConnect');
}
_onIbmConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onIbmConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
vendor: 'ibm',
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor IBM: ${reason}`});
this.notifyTaskDone();
}
_onIbmError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskGather:_onIbmError'); }
_onVadDetected(cs, ep) { _onVadDetected(cs, ep) {
if (this.bargein && this.minBargeinWordCount === 0) { if (this.bargein && this.minBargeinWordCount === 0) {
this.logger.debug('TaskGather:_onVadDetected'); this.logger.debug('TaskGather:_onVadDetected');
@@ -750,17 +463,10 @@ class TaskGather extends Task {
} }
} }
_onNoSpeechDetected(cs, ep, evt, fsEvent) { _onNoSpeechDetected(cs, ep) {
if (!this.callSession.callGone && !this.killed) { if (!this.callSession.callGone && !this.killed) {
const finished = fsEvent.getHeader('transcription-session-finished'); this.logger.debug('TaskGather:_onNoSpeechDetected - listen again');
if (this.vendor === 'microsoft' && finished === 'true') { return this._startTranscribing(ep);
this.logger.debug('TaskGather:_onNoSpeechDetected for old gather, ignoring');
}
else {
this.logger.debug('TaskGather:_onNoSpeechDetected - listen again');
this._startTranscribing(ep);
}
return;
} }
} }
@@ -770,26 +476,15 @@ class TaskGather extends Task {
this.resolved = true; this.resolved = true;
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearTimer();
if (this.isContinuousAsr && reason.startsWith('speech')) {
evt = {
is_final: true,
transcripts: this._bufferedTranscripts
};
this.logger.debug({evt}, 'TaskGather:resolve continuous asr');
}
else if (!this.isContinuousAsr && reason.startsWith('speech') && this._bufferedTranscripts.length) {
compileTranscripts(this.logger, evt, this._bufferedTranscripts);
this.logger.debug({evt}, 'TaskGather:resolve buffered results');
}
this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)}); this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
if (this.needsStt && this.ep && this.ep.connected) { if (this.ep && this.ep.connected) {
this.ep.stopTranscription({vendor: this.vendor}) this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.error({err}, 'Error stopping transcription')); .catch((err) => this.logger.error({err}, 'Error stopping transcription'));
} }
this._clearTimer();
if (this.callSession && this.callSession.callGone) { if (this.callSession && this.callSession.callGone) {
this.logger.debug('TaskGather:_resolve - call is gone, not invoking web callback'); this.logger.debug('TaskGather:_resolve - call is gone, not invoking web callback');
this.notifyTaskDone(); this.notifyTaskDone();

View File

@@ -14,11 +14,10 @@ class TaskHangup extends Task {
/** /**
* Hangup the call * Hangup the call
*/ */
async exec(cs, {dlg}) { async exec(cs, dlg) {
await super.exec(cs); await super.exec(cs);
try { try {
await dlg.destroy({headers: this.headers}); await dlg.destroy({headers: this.headers});
cs._callReleased();
} catch (err) { } catch (err) {
this.logger.error(err, 'TaskHangup:exec - Error hanging up call'); this.logger.error(err, 'TaskHangup:exec - Error hanging up call');
} }

View File

@@ -8,7 +8,7 @@ class TaskLeave extends Task {
get name() { return TaskName.Leave; } get name() { return TaskName.Leave; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
await this.awaitTaskDone(); await this.awaitTaskDone();
} }

View File

@@ -1,6 +1,6 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
class Lex extends Task { class Lex extends Task {
constructor(logger, opts) { constructor(logger, opts) {
@@ -44,7 +44,7 @@ class Lex extends Task {
get name() { return TaskName.Lex; } get name() { return TaskName.Lex; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
try { try {

View File

@@ -10,7 +10,7 @@ class TaskListen extends Task {
[ [
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep', 'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
'sampleRate', 'timeout', 'transcribe', 'wsAuth', 'disableBidirectionalAudio' 'sampleRate', 'timeout', 'transcribe', 'wsAuth'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
this.mixType = this.mixType || 'mono'; this.mixType = this.mixType || 'mono';
@@ -22,14 +22,15 @@ class TaskListen extends Task {
this.results = {}; this.results = {};
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this); if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
this._dtmfHandler = this._onDtmf.bind(this);
} }
get name() { return TaskName.Listen; } get name() { return TaskName.Listen; }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
this._dtmfHandler = this._onDtmf.bind(this, ep);
try { try {
this.hook = this.normalizeUrl(this.url, 'GET', this.wsAuth); this.hook = this.normalizeUrl(this.url, 'GET', this.wsAuth);
@@ -40,7 +41,7 @@ class TaskListen extends Task {
const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`);
this.transcribeTask.span = span; this.transcribeTask.span = span;
this.transcribeTask.ctx = ctx; this.transcribeTask.ctx = ctx;
this.transcribeTask.exec(cs, {ep}) this.transcribeTask.exec(cs, ep)
.then((result) => span.end()) .then((result) => span.end())
.catch((err) => span.end()); .catch((err) => span.end());
} }
@@ -60,21 +61,14 @@ class TaskListen extends Task {
this._clearTimer(); this._clearTimer();
if (this.ep && this.ep.connected) { if (this.ep && this.ep.connected) {
this.logger.debug('TaskListen:kill closing websocket'); this.logger.debug('TaskListen:kill closing websocket');
try { await this.ep.forkAudioStop()
await this.ep.forkAudioStop(); .catch((err) => this.logger.info(err, 'TaskListen:kill'));
this.logger.debug('TaskListen:kill successfully closed websocket');
} catch (err) {
this.logger.info(err, 'TaskListen:kill');
}
} }
if (this.recordStartTime) { if (this.recordStartTime) {
const duration = moment().diff(this.recordStartTime, 'seconds'); const duration = moment().diff(this.recordStartTime, 'seconds');
this.results.dialCallDuration = duration; this.results.dialCallDuration = duration;
} }
if (this.transcribeTask) { if (this.transcribeTask) await this.transcribeTask.kill(cs);
await this.transcribeTask.kill(cs);
this.transcribeTask = null;
}
this.ep && this._removeListeners(this.ep); this.ep && this._removeListeners(this.ep);
this.notifyTaskDone(); this.notifyTaskDone();
} }
@@ -136,9 +130,7 @@ class TaskListen extends Task {
} }
/* support bi-directional audio */ /* support bi-directional audio */
if (!this.disableBiDirectionalAudio) { ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
}
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep)); ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep)); ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
} }
@@ -156,13 +148,7 @@ class TaskListen extends Task {
} }
_onDtmf(ep, evt) { _onDtmf(evt) {
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${evt.dtmf}`);
if (this.passDtmf && this.ep?.connected) {
const obj = {event: 'dtmf', dtmf: evt.dtmf, duration: evt.duration};
this.ep.forkAudioSendText(obj)
.catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf'));
}
if (evt.dtmf === this.finishOnKey) { if (evt.dtmf === this.finishOnKey) {
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`); this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
this.results.digits = evt.dtmf; this.results.digits = evt.dtmf;
@@ -221,7 +207,7 @@ class TaskListen extends Task {
this.logger.debug('Listen:whisper tasks starting'); this.logger.debug('Listen:whisper tasks starting');
while (tasks.length && !cs.callGone) { while (tasks.length && !cs.callGone) {
const task = tasks.shift(); const task = tasks.shift();
await task.exec(cs, {ep: this.ep}); await task.exec(cs, this.ep);
} }
this.logger.debug('Listen:whisper tasks complete'); this.logger.debug('Listen:whisper tasks complete');
} catch (err) { } catch (err) {

View File

@@ -1,4 +1,4 @@
const { validateVerb } = require('@jambonz/verb-specifications'); const Task = require('./task');
const {TaskName} = require('../utils/constants'); const {TaskName} = require('../utils/constants');
const errBadInstruction = new Error('malformed jambonz application payload'); const errBadInstruction = new Error('malformed jambonz application payload');
@@ -12,14 +12,11 @@ function makeTask(logger, obj, parent) {
if (typeof data !== 'object') { if (typeof data !== 'object') {
throw errBadInstruction; throw errBadInstruction;
} }
validateVerb(name, data, logger); Task.validate(name, data);
switch (name) { switch (name) {
case TaskName.SipDecline: case TaskName.SipDecline:
const TaskSipDecline = require('./sip_decline'); const TaskSipDecline = require('./sip_decline');
return new TaskSipDecline(logger, data, parent); return new TaskSipDecline(logger, data, parent);
case TaskName.SipRequest:
const TaskSipRequest = require('./sip_request');
return new TaskSipRequest(logger, data, parent);
case TaskName.SipRefer: case TaskName.SipRefer:
const TaskSipRefer = require('./sip_refer'); const TaskSipRefer = require('./sip_refer');
return new TaskSipRefer(logger, data, parent); return new TaskSipRefer(logger, data, parent);

View File

@@ -1,7 +1,7 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const bent = require('bent'); const bent = require('bent');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
class TaskMessage extends Task { class TaskMessage extends Task {
constructor(logger, opts) { constructor(logger, opts) {

View File

@@ -10,7 +10,7 @@ class TaskPause extends Task {
get name() { return TaskName.Pause; } get name() { return TaskName.Pause; }
async exec(cs) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
this.timer = setTimeout(this.notifyTaskDone.bind(this), this.length * 1000); this.timer = setTimeout(this.notifyTaskDone.bind(this), this.length * 1000);
await this.awaitTaskDone(); await this.awaitTaskDone();

View File

@@ -7,8 +7,6 @@ class TaskPlay extends Task {
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.url = this.data.url; this.url = this.data.url;
this.seekOffset = this.data.seekOffset || -1;
this.timeoutSecs = this.data.timeoutSecs || -1;
this.loop = this.data.loop || 1; this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true; this.earlyMedia = this.data.earlyMedia === true;
} }
@@ -19,54 +17,18 @@ class TaskPlay extends Task {
return `${this.name}:{url=${this.url}}`; return `${this.name}:{url=${this.url}}`;
} }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
let timeout;
let playbackSeconds = 0;
let playbackMilliseconds = 0;
let completed = !(this.timeoutSecs > 0 || this.loop);
if (this.timeoutSecs > 0) {
timeout = setTimeout(async() => {
completed = true;
try {
await this.kill(cs);
} catch (err) {
this.logger.info(err, 'Error killing audio on timeoutSecs');
}
}, this.timeoutSecs * 1000);
}
try { try {
this.notifyStatus({event: 'start-playback'});
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) { while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
if (cs.isInConference) { if (cs.isInConference) {
const {memberId, confName, confUuid} = cs; const {memberId, confName, confUuid} = cs;
if (Array.isArray(this.url)) { await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
for (const playUrl of this.url) {
await this.playToConfMember(this.ep, memberId, confName, confUuid, playUrl);
}
} else {
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
}
} else {
let file = this.url;
if (this.seekOffset >= 0) {
file = {file: this.url, seekOffset: this.seekOffset};
this.seekOffset = -1;
}
const result = await ep.play(file);
playbackSeconds += parseInt(result.playbackSeconds);
playbackMilliseconds += parseInt(result.playbackMilliseconds);
if (this.killed || !this.loop || completed) {
if (timeout) clearTimeout(timeout);
await this.performAction(
Object.assign(result, {reason: 'playCompleted', playbackSeconds, playbackMilliseconds}),
!(this.parentTask || cs.isConfirmCallSession));
}
} }
else await ep.play(this.url);
} }
} catch (err) { } catch (err) {
if (timeout) clearTimeout(timeout);
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`); this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
} }
this.emit('playDone'); this.emit('playDone');
@@ -81,8 +43,7 @@ class TaskPlay extends Task {
this.killPlayToConfMember(this.ep, memberId, confName); this.killPlayToConfMember(this.ep, memberId, confName);
} }
else { else {
this.notifyStatus({event: 'kill-playback'}); await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
} }
} }
} }

View File

@@ -20,7 +20,7 @@ class Rasa extends Task {
return this.reportedFinalAction || this.isReplacingApplication; return this.reportedFinalAction || this.isReplacingApplication;
} }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
@@ -34,7 +34,7 @@ class Rasa extends Task {
const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`);
this.gatherTask.span = span; this.gatherTask.span = span;
this.gatherTask.ctx = ctx; this.gatherTask.ctx = ctx;
this.gatherTask.exec(cs, {ep}) this.gatherTask.exec(cs, ep, this)
.then(() => span.end()) .then(() => span.end())
.catch((err) => { .catch((err) => {
span.end(); span.end();
@@ -128,7 +128,7 @@ class Rasa extends Task {
const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`);
this.gatherTask.span = span; this.gatherTask.span = span;
this.gatherTask.ctx = ctx; this.gatherTask.ctx = ctx;
this.gatherTask.exec(cs, {ep}) this.gatherTask.exec(cs, ep, this)
.then(() => span.end()) .then(() => span.end())
.catch((err) => { .catch((err) => {
span.end(); span.end();

View File

@@ -1,7 +1,7 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName} = require('../utils/constants'); const {TaskName} = require('../utils/constants');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
/** /**
* Manages an outdial made via REST API * Manages an outdial made via REST API
@@ -11,7 +11,6 @@ class TaskRestDial extends Task {
super(logger, opts); super(logger, opts);
this.from = this.data.from; this.from = this.data.from;
this.fromHost = this.data.fromHost;
this.to = this.data.to; this.to = this.data.to;
this.call_hook = this.data.call_hook; this.call_hook = this.data.call_hook;
this.timeout = this.data.timeout || 60; this.timeout = this.data.timeout || 60;
@@ -27,7 +26,7 @@ class TaskRestDial extends Task {
*/ */
async exec(cs) { async exec(cs) {
await super.exec(cs); await super.exec(cs);
this.canCancel = true; this.req = cs.req;
this._setCallTimer(); this._setCallTimer();
await this.awaitTaskDone(); await this.awaitTaskDone();
@@ -36,15 +35,15 @@ class TaskRestDial extends Task {
kill(cs) { kill(cs) {
super.kill(cs); super.kill(cs);
this._clearCallTimer(); this._clearCallTimer();
if (this.canCancel && cs?.req) { if (this.req) {
this.canCancel = false; this.req.cancel();
cs.req.cancel(); this.req = null;
} }
this.notifyTaskDone(); this.notifyTaskDone();
} }
async _onConnect(dlg) { async _onConnect(dlg) {
this.canCancel = false; this.req = null;
const cs = this.callSession; const cs = this.callSession;
cs.setDialog(dlg); cs.setDialog(dlg);
@@ -79,7 +78,7 @@ class TaskRestDial extends Task {
_onCallStatus(status) { _onCallStatus(status) {
this.logger.debug(`CallStatus: ${status}`); this.logger.debug(`CallStatus: ${status}`);
if (status >= 200) { if (status >= 200) {
this.canCancel = false; this.req = null;
this._clearCallTimer(); this._clearCallTimer();
if (status !== 200) this.notifyTaskDone(); if (status !== 200) this.notifyTaskDone();
} }

View File

@@ -23,7 +23,7 @@ class TaskSayLegacy extends Task {
get name() { return TaskName.SayLegacy; } get name() { return TaskName.SayLegacy; }
async exec(cs, {ep}) { async exec(cs, ep) {
super.exec(cs); super.exec(cs);
this.ep = ep; this.ep = ep;
try { try {

View File

@@ -1,41 +1,15 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const pollySSMLSplit = require('polly-ssml-split');
const breakLengthyTextIfNeeded = (logger, text) => {
const chunkSize = 1000;
const isSSML = text.startsWith('<speak>');
if (text.length <= chunkSize || !isSSML) return [text];
const options = {
// MIN length
softLimit: 100,
// MAX length, exclude 15 characters <speak></speak>
hardLimit: chunkSize - 15,
// Set of extra split characters (Optional property)
extraSplitChars: ',;!?',
};
pollySSMLSplit.configure(options);
try {
return pollySSMLSplit.split(text);
} catch (err) {
logger.info({err}, 'Error spliting SSML long text');
return [text];
}
};
class TaskSay extends Task { class TaskSay extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
super(logger, opts); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.text = (Array.isArray(this.data.text) ? this.data.text : [this.data.text]) this.text = Array.isArray(this.data.text) ? this.data.text : [this.data.text];
.map((t) => breakLengthyTextIfNeeded(this.logger, t))
.flat();
this.loop = this.data.loop || 1; this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia); this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
this.synthesizer = this.data.synthesizer || {}; this.synthesizer = this.data.synthesizer || {};
this.disableTtsCache = this.data.disableTtsCache;
} }
get name() { return TaskName.Say; } get name() { return TaskName.Say; }
@@ -48,7 +22,7 @@ class TaskSay extends Task {
return `${this.name}{${this.text[0]}}`; return `${this.name}{${this.text[0]}}`;
} }
async exec(cs, {ep}) { async exec(cs, ep) {
await super.exec(cs); await super.exec(cs);
const {srf} = cs; const {srf} = cs;
@@ -61,24 +35,14 @@ class TaskSay extends Task {
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ? const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
this.synthesizer.language : this.synthesizer.language :
cs.speechSynthesisLanguage ; cs.speechSynthesisLanguage ;
let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ? const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
this.synthesizer.voice : this.synthesizer.voice :
cs.speechSynthesisVoice; cs.speechSynthesisVoice;
const engine = this.synthesizer.engine || 'standard'; const engine = this.synthesizer.engine || 'standard';
const salt = cs.callSid; const salt = cs.callSid;
const credentials = cs.getSpeechCredentials(vendor, 'tts'); const credentials = cs.getSpeechCredentials(vendor, 'tts');
/* parse Nuance voices into name and model */ this.logger.info({vendor, language, voice}, 'TaskSay:exec');
let model;
if (vendor === 'nuance' && voice) {
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
if (arr) {
voice = arr[1];
model = arr[2];
}
}
this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
this.ep = ep; this.ep = ep;
try { try {
if (!credentials) { if (!credentials) {
@@ -87,10 +51,7 @@ class TaskSay extends Task {
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
this.notifyError({ this.notifyError(`No speech credentials have been provisioned for ${vendor}`);
msg: 'TTS error',
details:`No speech credentials provisioned for selected vendor ${vendor}`
});
throw new Error('no provisioned speech credentials for TTS'); throw new Error('no provisioned speech credentials for TTS');
} }
// synthesize all of the text elements // synthesize all of the text elements
@@ -108,16 +69,14 @@ class TaskSay extends Task {
'tts.voice': voice 'tts.voice': voice
}); });
try { try {
const {filePath, servedFromCache, rtt} = await synthAudio(stats, { const {filePath, servedFromCache} = await synthAudio(stats, {
text, text,
vendor, vendor,
language, language,
voice, voice,
engine, engine,
model,
salt, salt,
credentials, credentials
disableTtsCache : this.disableTtsCache
}); });
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`); this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
if (filePath) cs.trackTmpFile(filePath); if (filePath) cs.trackTmpFile(filePath);
@@ -128,33 +87,24 @@ class TaskSay extends Task {
} }
span.setAttributes({'tts.cached': servedFromCache}); span.setAttributes({'tts.cached': servedFromCache});
span.end(); span.end();
if (!servedFromCache && rtt) {
this.notifyStatus({
event: 'synthesized-audio',
vendor,
language,
characters: text.length,
elapsedTime: rtt
});
}
return filePath; return filePath;
} catch (err) { } catch (err) {
this.logger.info({err}, 'Error synthesizing tts'); this.logger.info({err}, 'Error synthesizing tts');
span.end(); span.end();
writeAlerts({ writeAlerts({
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor, vendor,
detail: err.message detail: err.message
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.notifyError({msg: 'TTS error', details: err.message || err}); this.notifyError(err.message || err);
return; return;
} }
}; };
const arr = this.text.map((t) => generateAudio(t)); const arr = this.text.map((t) => generateAudio(t));
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length); const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
this.notifyStatus({event: 'start-playback'}); this.logger.debug({filepath}, 'synthesized files for tts');
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) { while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
let segment = 0; let segment = 0;
@@ -186,7 +136,6 @@ class TaskSay extends Task {
this.killPlayToConfMember(this.ep, memberId, confName); this.killPlayToConfMember(this.ep, memberId, confName);
} }
else { else {
this.notifyStatus({event: 'kill-playback'});
this.ep.api('uuid_break', this.ep.uuid); this.ep.api('uuid_break', this.ep.uuid);
} }
} }

View File

@@ -36,7 +36,6 @@ class TaskSipRefer extends Task {
method: 'REFER', method: 'REFER',
headers: { headers: {
...this.headers, ...this.headers,
...(this.referToIsUri && {'X-Refer-To-Leave-Untouched': true}),
'Refer-To': referTo, 'Refer-To': referTo,
'Referred-By': referredBy 'Referred-By': referredBy
} }
@@ -47,17 +46,7 @@ class TaskSipRefer extends Task {
/* if we fail, fall through to next verb. If success, we should get BYE from far end */ /* if we fail, fall through to next verb. If success, we should get BYE from far end */
if (this.referStatus === 202) { if (this.referStatus === 202) {
this._notifyTimer = setTimeout(() => {
this.logger.info('TaskSipRefer:exec - no NOTIFY received in 15 secs, exiting');
this.performAction({refer_status: this.referStatus})
.catch((err) => this.logger.error(err, 'TaskSipRefer:exec - error performing action'));
this.notifyTaskDone();
}, 15000);
await this.awaitTaskDone(); await this.awaitTaskDone();
if (this._notifyTimer) {
clearTimeout(this._notifyTimer);
this._notifyTimer = null;
}
} }
else { else {
await this.performAction({refer_status: this.referStatus}); await this.performAction({refer_status: this.referStatus});
@@ -81,10 +70,10 @@ class TaskSipRefer extends Task {
const contentType = req.get('Content-Type'); const contentType = req.get('Content-Type');
this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`); this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`);
if (contentType?.includes('message/sipfrag')) { if (contentType === 'message/sipfrag') {
const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body); const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body);
if (arr) { if (arr) {
const status = typeof arr[1] === 'string' ? parseInt(arr[1], 10) : arr[1]; const status = arr[1];
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`); this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
if (this.eventHook) { if (this.eventHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
@@ -111,7 +100,6 @@ class TaskSipRefer extends Task {
/* they may have only provided a phone number/user */ /* they may have only provided a phone number/user */
referTo = `sip:${referTo}@${host}`; referTo = `sip:${referTo}@${host}`;
} }
else this.referToIsUri = true;
if (!referredBy) { if (!referredBy) {
/* default */ /* default */
referredBy = cs.req?.callingNumber || dlg.local.uri; referredBy = cs.req?.callingNumber || dlg.local.uri;

View File

@@ -1,49 +0,0 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
/**
* Send a SIP request (e.g. INFO, NOTIFY, etc) on an existing call leg
*/
class TaskSipRequest extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.StableCall;
this.method = this.data.method.toUpperCase();
this.headers = this.data.headers || {};
this.body = this.data.body;
if (this.body) this.body = `${this.body}\n`;
}
get name() { return TaskName.SipRequest; }
async exec(cs, {dlg}) {
super.exec(cs);
try {
this.logger.info({dlg}, `TaskSipRequest: sending a SIP ${this.method}`);
const res = await dlg.request({
method: this.method,
headers: this.headers,
body: this.body
});
const result = {result: 'success', sipStatus: res.status};
this.span.setAttributes({
...this.headers,
...(this.body && {body: this.body}),
'response.status_code': res.status
});
this.logger.debug({result}, `TaskSipRequest: received response to ${this.method}`);
await this.performAction(result);
} catch (err) {
this.logger.error({err}, 'TaskSipRequest: error');
this.span.setAttributes({
...this.headers,
...(this.body && {body: this.body}),
'response.error': err.message
});
await this.performAction({result: 'failed', err: err.message});
}
}
}
module.exports = TaskSipRequest;

494
lib/tasks/specs.json Normal file
View File

@@ -0,0 +1,494 @@
{
"sip:decline": {
"properties": {
"status": "number",
"reason": "string",
"headers": "object"
},
"required": [
"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",
"sticky": "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",
"actionHook": "object|string",
"timeout": "number",
"beep": "boolean"
},
"required": [
"name"
]
},
"enqueue": {
"properties": {
"name": "string",
"actionHook": "object|string",
"waitHook": "object|string",
"_": "object"
},
"required": [
"name"
]
},
"leave": {
"properties": {
}
},
"hangup": {
"properties": {
"headers": "object"
},
"required": [
]
},
"play": {
"properties": {
"url": "string",
"loop": "number|string",
"earlyMedia": "boolean"
},
"required": [
"url"
]
},
"say": {
"properties": {
"text": "string|array",
"loop": "number|string",
"synthesizer": "#synthesizer",
"earlyMedia": "boolean"
},
"required": [
"text"
]
},
"gather": {
"properties": {
"actionHook": "object|string",
"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": [
]
},
"conference": {
"properties": {
"name": "string",
"beep": "boolean",
"startConferenceOnEnter": "boolean",
"endConferenceOnExit": "boolean",
"maxParticipants": "number",
"joinMuted": "boolean",
"actionHook": "object|string",
"waitHook": "object|string",
"statusEvents": "array",
"statusHook": "object|string",
"enterHook": "object|string",
"record": "#record"
},
"required": [
"name"
]
},
"dial": {
"properties": {
"actionHook": "object|string",
"answerOnBridge": "boolean",
"callerId": "string",
"confirmHook": "object|string",
"referHook": "object|string",
"dialMusic": "string",
"dtmfCapture": "object",
"dtmfHook": "object|string",
"headers": "object",
"listen": "#listen",
"target": ["#target"],
"timeLimit": "number",
"timeout": "number",
"proxy": "string",
"transcribe": "#transcribe"
},
"required": [
"target"
]
},
"dialogflow": {
"properties": {
"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",
"events": "[string]",
"welcomeEvent": "string",
"welcomeEventParams": "object",
"noInputTimeout": "number",
"noInputEvent": "string",
"passDtmfAsTextInput": "boolean",
"thinkingMusic": "string",
"tts": "#synthesizer",
"bargein": "boolean"
},
"required": [
"project",
"credentials",
"lang"
]
},
"dtmf": {
"properties": {
"dtmf": "string",
"duration": "number"
},
"required": [
"dtmf"
]
},
"lex": {
"properties": {
"botId": "string",
"botAlias": "string",
"credentials": "object",
"region": "string",
"locale": "string",
"intent": "#lexIntent",
"welcomeMessage": "string",
"metadata": "object",
"bargein": "boolean",
"passDtmf": "boolean",
"actionHook": "object|string",
"eventHook": "object|string",
"noInputTimeout": "number",
"tts": "#synthesizer"
},
"required": [
"botId",
"botAlias",
"region",
"credentials"
]
},
"listen": {
"properties": {
"actionHook": "object|string",
"auth": "#auth",
"finishOnKey": "string",
"maxLength": "number",
"metadata": "object",
"mixType": {
"type": "string",
"enum": ["mono", "stereo", "mixed"]
},
"passDtmf": "boolean",
"playBeep": "boolean",
"sampleRate": "number",
"timeout": "number",
"transcribe": "#transcribe",
"url": "string",
"wsAuth": "#auth",
"earlyMedia": "boolean"
},
"required": [
"url"
]
},
"message": {
"properties": {
"carrier": "string",
"account_sid": "string",
"message_sid": "string",
"to": "string",
"from": "string",
"text": "string",
"media": "string|array",
"actionHook": "object|string"
},
"required": [
"to",
"from"
]
},
"pause": {
"properties": {
"length": "number"
},
"required": [
"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"
},
"required": [
"actionHook"
]
},
"rest:dial": {
"properties": {
"account_sid": "string",
"application_sid": "string",
"call_hook": "object|string",
"call_status_hook": "object|string",
"from": "string",
"speech_synthesis_vendor": "string",
"speech_synthesis_voice": "string",
"speech_synthesis_language": "string",
"speech_recognizer_vendor": "string",
"speech_recognizer_language": "string",
"tag": "object",
"to": "#target",
"headers": "object",
"timeout": "number"
},
"required": [
"call_hook",
"from",
"to"
]
},
"tag": {
"properties": {
"data": "object"
},
"required": [
"data"
]
},
"transcribe": {
"properties": {
"transcriptionHook": "string",
"recognizer": "#recognizer",
"earlyMedia": "boolean"
},
"required": [
"recognizer"
]
},
"target": {
"properties": {
"type": {
"type": "string",
"enum": ["phone", "sip", "user", "teams"]
},
"confirmHook": "object|string",
"method": {
"type": "string",
"enum": ["GET", "POST"]
},
"headers": "object",
"name": "string",
"number": "string",
"sipUri": "string",
"auth": "#auth",
"vmail": "boolean",
"tenant": "string",
"trunk": "string",
"overrideTo": "string"
},
"required": [
"type"
]
},
"auth": {
"properties": {
"username": "string",
"password": "string"
},
"required": [
"username",
"password"
]
},
"synthesizer": {
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "polly", "microsoft", "default"]
},
"language": "string",
"voice": "string",
"engine": {
"type": "string",
"enum": ["standard", "neural"]
},
"gender": {
"type": "string",
"enum": ["MALE", "FEMALE", "NEUTRAL"]
}
},
"required": [
"vendor"
]
},
"recognizer": {
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "microsoft", "default"]
},
"language": "string",
"vad": "#vad",
"hints": "array",
"hintsBoost": "number",
"altLanguages": "array",
"profanityFilter": "boolean",
"interim": "boolean",
"singleUtterance": "boolean",
"dualChannel": "boolean",
"separateRecognitionPerChannel": "boolean",
"punctuation": "boolean",
"enhancedModel": "boolean",
"words": "boolean",
"diarization": "boolean",
"diarizationMinSpeakers": "number",
"diarizationMaxSpeakers": "number",
"interactionType": {
"type": "string",
"enum": [
"unspecified",
"discussion",
"presentation",
"phone_call",
"voicemail",
"voice_search",
"voice_command",
"dictation"
]
},
"naicsCode": "number",
"identifyChannels": "boolean",
"vocabularyName": "string",
"vocabularyFilterName": "string",
"filterMethod": {
"type": "string",
"enum": [
"remove",
"mask",
"tag"
]
},
"model": "string",
"outputFormat": {
"type": "string",
"enum": [
"simple",
"detailed"
]
},
"profanityOption": {
"type": "string",
"enum": [
"masked",
"removed",
"raw"
]
},
"requestSnr": "boolean",
"initialSpeechTimeoutMs": "number",
"azureServiceEndpoint": "string"
},
"required": [
"vendor"
]
},
"lexIntent": {
"properties": {
"name": "string",
"slots": "object"
},
"required": [
"name"
]
},
"vad": {
"properties": {
"enable": "boolean",
"voiceMs": "number",
"mode": "number"
},
"required": [
"enable"
]
}
}

View File

@@ -1,10 +1,13 @@
const Emitter = require('events'); const Emitter = require('events');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const debug = require('debug')('jambonz:feature-server');
const assert = require('assert');
const {TaskPreconditions} = require('../utils/constants'); const {TaskPreconditions} = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const WsRequestor = require('../utils/ws-requestor');
const {TaskName} = require('../utils/constants');
const {trace} = require('@opentelemetry/api'); const {trace} = require('@opentelemetry/api');
const specs = new Map();
const _specData = require('./specs');
for (const key in _specData) {specs.set(key, _specData[key]);}
/** /**
* @classdesc Represents a jambonz verb. This is a superclass that is extended * @classdesc Represents a jambonz verb. This is a superclass that is extended
@@ -18,7 +21,6 @@ class Task extends Emitter {
this.logger = logger; this.logger = logger;
this.data = data; this.data = data;
this.actionHook = this.data.actionHook; this.actionHook = this.data.actionHook;
this.id = data.id;
this._killInProgress = false; this._killInProgress = false;
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve); this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
@@ -135,32 +137,21 @@ class Task extends Emitter {
return this.callSession.normalizeUrl(url, method, auth); return this.callSession.normalizeUrl(url, method, auth);
} }
notifyError(obj) { notifyError(errMsg) {
if (this.cs.requestor instanceof WsRequestor) { const params = {error: errMsg, verb: this.name};
const params = {...obj, verb: this.name, id: this.id}; this.cs.requestor.request('jambonz:error', '/error', params)
this.cs.requestor.request('jambonz:error', '/error', params) .catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
.catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
}
}
notifyStatus(obj) {
if (this.cs.notifyEvents && this.cs.requestor instanceof WsRequestor) {
const params = {...obj, verb: this.name, id: this.id};
this.cs.requestor.request('verb:status', '/status', params)
.catch((err) => this.logger.info({err}, 'Task:notifyStatus error sending error'));
}
} }
async performAction(results, expectResponse = true) { async performAction(results, expectResponse = true) {
if (this.actionHook) { if (this.actionHook) {
const type = this.name === TaskName.Redirect ? 'session:redirect' : 'verb:hook'; const params = results ? Object.assign(results, this.cs.callInfo.toJSON()) : this.cs.callInfo.toJSON();
const params = results ? Object.assign(this.cs.callInfo.toJSON(), results) : this.cs.callInfo.toJSON(); const span = this.startSpan('verb:hook', {'hook.url': this.actionHook});
const span = this.startSpan(type, {'hook.url': this.actionHook});
const b3 = this.getTracingPropagation('b3', span); const b3 = this.getTracingPropagation('b3', span);
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
span.setAttributes({'http.body': JSON.stringify(params)}); span.setAttributes({'http.body': JSON.stringify(params)});
try { try {
const json = await this.cs.requestor.request(type, this.actionHook, params, httpHeaders); const json = await this.cs.requestor.request('verb:hook', this.actionHook, params, httpHeaders);
span.setAttributes({'http.statusCode': 200}); span.setAttributes({'http.statusCode': 200});
span.end(); span.end();
if (expectResponse && json && Array.isArray(json)) { if (expectResponse && json && Array.isArray(json)) {
@@ -180,13 +171,12 @@ class Task extends Emitter {
} }
async performHook(cs, hook, results) { async performHook(cs, hook, results) {
const params = results ? Object.assign(cs.callInfo.toJSON(), results) : cs.callInfo.toJSON();
const span = this.startSpan('verb:hook', {'hook.url': hook}); const span = this.startSpan('verb:hook', {'hook.url': hook});
const b3 = this.getTracingPropagation('b3', span); const b3 = this.getTracingPropagation('b3', span);
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
span.setAttributes({'http.body': JSON.stringify(params)}); span.setAttributes({'http.body': JSON.stringify(results)});
try { try {
const json = await cs.requestor.request('verb:hook', hook, params, httpHeaders); const json = await cs.requestor.request('verb:hook', hook, results, httpHeaders);
span.setAttributes({'http.statusCode': 200}); span.setAttributes({'http.statusCode': 200});
span.end(); span.end();
if (json && Array.isArray(json)) { if (json && Array.isArray(json)) {
@@ -282,6 +272,74 @@ class Task extends Emitter {
this.logger.error(err, 'Task:_doRefer error'); this.logger.error(err, 'Task:_doRefer error');
} }
} }
/**
* validate that the JSON task description is valid
* @param {string} name - verb name
* @param {object} data - verb properties
*/
static validate(name, data) {
debug(`validating ${name} with data ${JSON.stringify(data)}`);
// validate the instruction is supported
if (!specs.has(name)) throw new Error(`invalid instruction: ${name}`);
// check type of each element and make sure required elements are present
const specData = specs.get(name);
let required = specData.required || [];
for (const dKey in data) {
if (dKey in specData.properties) {
const dVal = data[dKey];
const dSpec = specData.properties[dKey];
debug(`Task:validate validating property ${dKey} with value ${JSON.stringify(dVal)}`);
if (typeof dSpec === 'string' && dSpec === 'array') {
if (!Array.isArray(dVal)) throw new Error(`${name}: property ${dKey} is not an array`);
}
else if (typeof dSpec === 'string' && dSpec.includes('|')) {
const types = dSpec.split('|').map((t) => t.trim());
if (!types.includes(typeof dVal) && !(types.includes('array') && Array.isArray(dVal))) {
throw new Error(`${name}: property ${dKey} has invalid data type, must be one of ${types}`);
}
}
else if (typeof dSpec === 'string' && ['number', 'string', 'object', 'boolean'].includes(dSpec)) {
// simple types
if (typeof dVal !== specData.properties[dKey]) {
throw new Error(`${name}: property ${dKey} has invalid data type`);
}
}
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
const name = dSpec[0].slice(1);
for (const item of dVal) {
Task.validate(name, item);
}
}
else if (typeof dSpec === 'object') {
// complex types
const type = dSpec.type;
assert.ok(['number', 'string', 'object', 'boolean'].includes(type),
`invalid or missing type in spec ${JSON.stringify(dSpec)}`);
if (type === 'string' && dSpec.enum) {
assert.ok(Array.isArray(dSpec.enum), `enum must be an array ${JSON.stringify(dSpec.enum)}`);
if (!dSpec.enum.includes(dVal)) throw new Error(`invalid value ${dVal} must be one of ${dSpec.enum}`);
}
}
else if (typeof dSpec === 'string' && dSpec.startsWith('#')) {
// reference to another datatype (i.e. nested type)
const name = dSpec.slice(1);
//const obj = {};
//obj[name] = dVal;
Task.validate(name, dVal);
}
else {
assert.ok(0, `invalid spec ${JSON.stringify(dSpec)}`);
}
required = required.filter((item) => item !== dKey);
}
else throw new Error(`${name}: unknown property ${dKey}`);
}
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);
}
} }
module.exports = Task; module.exports = Task;

View File

@@ -3,16 +3,9 @@ const {
TaskName, TaskName,
TaskPreconditions, TaskPreconditions,
GoogleTranscriptionEvents, GoogleTranscriptionEvents,
NuanceTranscriptionEvents,
AwsTranscriptionEvents,
AzureTranscriptionEvents, AzureTranscriptionEvents,
DeepgramTranscriptionEvents, AwsTranscriptionEvents
SonioxTranscriptionEvents,
IbmTranscriptionEvents,
NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('../utils/constants'); } = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications');
class TaskTranscribe extends Task { class TaskTranscribe extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
@@ -20,18 +13,6 @@ class TaskTranscribe extends Task {
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.parentTask = parentTask; this.parentTask = parentTask;
const {
setChannelVarsForStt,
normalizeTranscription,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
} = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription;
this.removeSpeechListeners = removeSpeechListeners;
this.compileSonioxTranscripts = compileSonioxTranscripts;
this.transcriptionHook = this.data.transcriptionHook; this.transcriptionHook = this.data.transcriptionHook;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia); this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
@@ -41,53 +22,51 @@ class TaskTranscribe extends Task {
this.interim = !!recognizer.interim; this.interim = !!recognizer.interim;
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel; this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
/* let credentials be supplied in the recognizer object at runtime */ /* vad: if provided, we dont connect to recognizer until voice activity is detected */
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer); const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
this.vad = {enable, voiceMs, mode};
/* buffer for soniox transcripts */ /* google-specific options */
this._sonioxTranscripts = []; this.hints = recognizer.hints || [];
this.hintsBoost = recognizer.hintsBoost;
this.profanityFilter = recognizer.profanityFilter;
this.punctuation = !!recognizer.punctuation;
this.enhancedModel = !!recognizer.enhancedModel;
this.model = recognizer.model || 'phone_call';
this.words = !!recognizer.words;
this.singleUtterance = recognizer.singleUtterance || false;
this.diarization = !!recognizer.diarization;
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
this.interactionType = recognizer.interactionType || 'unspecified';
this.naicsCode = recognizer.naicsCode || 0;
this.altLanguages = recognizer.altLanguages || [];
recognizer.hints = recognizer.hints || []; /* aws-specific options */
recognizer.altLanguages = recognizer.altLanguages || []; this.identifyChannels = !!recognizer.identifyChannels;
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; } get name() { return TaskName.Transcribe; }
async exec(cs, {ep, ep2}) { async exec(cs, ep, ep2) {
super.exec(cs); super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
if (cs.hasGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints;
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints);
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost;
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
'Transcribe:exec - applying global sttHints');
}
if (cs.hasAltLanguages) {
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'Transcribe:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
this.data.recognizer.punctuation = cs.globalSttPunctuation;
}
this.ep = ep; this.ep = ep;
this.ep2 = ep2; this.ep2 = ep2;
if ('default' === this.vendor || !this.vendor) { if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
this.vendor = cs.speechRecognizerVendor; if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor; this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (!this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
try { try {
if (!this.sttCredentials) { if (!this.sttCredentials) {
@@ -100,26 +79,8 @@ class TaskTranscribe extends Task {
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
throw new Error('no provisioned speech credentials for TTS'); throw new Error('no provisioned speech credentials for TTS');
} }
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
/* get nuance access token */
const {client_id, secret} = this.sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
this.logger.debug({client_id},
`Transcribe:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token};
}
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
/* get ibm access token */
const {stt_api_key, stt_region} = this.sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
}
await this._startTranscribing(cs, ep, 1); await this._startTranscribing(cs, ep, 1);
if (this.separateRecognitionPerChannel && ep2) { if (this.separateRecognitionPerChannel && ep2) await this._startTranscribing(cs, ep2, 2);
await this._startTranscribing(cs, ep2, 2);
}
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid) updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */}); .catch(() => {/*already logged error */});
@@ -129,112 +90,139 @@ class TaskTranscribe extends Task {
this.logger.info(err, 'TaskTranscribe:exec - error'); this.logger.info(err, 'TaskTranscribe:exec - error');
this.parentTask && this.parentTask.emit('error', err); this.parentTask && this.parentTask.emit('error', err);
} }
this.removeSpeechListeners(ep); ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
} }
async kill(cs) { async kill(cs) {
super.kill(cs); super.kill(cs);
let stopTranscription = false; if (this.ep.connected) {
if (this.ep?.connected) {
stopTranscription = true;
this.ep.stopTranscription({vendor: this.vendor}) this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
// hangup after 1 sec if we don't get a final transcription
this._timer = setTimeout(() => this.notifyTaskDone(), 1000);
} }
if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) { if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) {
stopTranscription = true;
this.ep2.stopTranscription({vendor: this.vendor}) this.ep2.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
} }
// hangup after 1 sec if we don't get a final transcription
if (stopTranscription) this._timer = setTimeout(() => this.notifyTaskDone(), 1500);
else this.notifyTaskDone(); else this.notifyTaskDone();
await this.awaitTaskDone(); await this.awaitTaskDone();
} }
async _startTranscribing(cs, ep, channel) { async _startTranscribing(cs, ep, channel) {
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer); const opts = {};
switch (this.vendor) {
case 'google':
this.bugname = 'google_transcribe';
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected,
this._onNoAudio.bind(this, cs, ep, channel));
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
break;
case 'aws': if (this.vad.enable) {
case 'polly': opts.START_RECOGNIZING_ON_VAD = 1;
this.bugname = 'aws_transcribe'; if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected,
this._onNoAudio.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
break;
case 'microsoft':
this.bugname = 'azure_transcribe';
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected,
this._onNoAudio.bind(this, cs, ep, channel));
break;
case 'nuance':
this.bugname = 'nuance_transcribe';
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep, channel));
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep, channel));
break;
case 'deepgram':
this.bugname = 'deepgram_transcribe';
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect,
this._onDeepgramConnect.bind(this, cs, ep, channel));
ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
this._onDeepGramConnectFailure.bind(this, cs, ep, channel));
break;
case 'soniox':
this.bugname = 'soniox_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
break;
case 'ibm':
this.bugname = 'ibm_transcribe';
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(IbmTranscriptionEvents.Connect,
this._onIbmConnect.bind(this, cs, ep, channel));
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onIbmConnectFailure.bind(this, cs, ep, channel));
break;
case 'nvidia':
this.bugname = 'nvidia_transcribe';
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
break;
default:
throw new Error(`Invalid vendor ${this.vendor}`);
} }
/* common handler for all stt engine errors */ ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep)); this._onTranscription.bind(this, cs, ep, channel));
await ep.set(opts) ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
.catch((err) => this.logger.info(err, 'Error setting channel variables')); ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
if (this.vendor === 'google') {
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
//['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
['singleUtterance', 'GOOGLE_SPEECH_SINGLE_UTTERANCE'],
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].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.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
if ('unspecified' !== this.interactionType) {
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
}
opts.GOOGLE_SPEECH_MODEL = this.model;
if (this.diarization && this.diarizationMinSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
}
if (this.diarization && this.diarizationMaxSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
}
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
}
else if (this.vendor === 'aws') {
[
['diarization', 'AWS_SHOW_SPEAKER_LABEL'],
['identifyChannels', 'AWS_ENABLE_CHANNEL_IDENTIFICATION']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
});
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
});
}
else {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_REGION: process.env.AWS_REGION
});
}
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.altLanguages.length > 1) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.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); await this._transcribe(ep);
} }
@@ -243,66 +231,50 @@ class TaskTranscribe extends Task {
vendor: this.vendor, vendor: this.vendor,
interim: this.interim ? true : false, interim: this.interim ? true : false,
locale: this.language, locale: this.language,
channels: /*this.separateRecognitionPerChannel ? 2 : */ 1, channels: /*this.separateRecognitionPerChannel ? 2 : */ 1
bugname: this.bugname
}); });
} }
async _onTranscription(cs, ep, channel, evt, fsEvent) { _onTranscription(cs, ep, channel, evt) {
// make sure this is not a transcript from answering machine detection this.logger.debug({evt, channel}, 'TaskTranscribe:_onTranscription');
const bugname = fsEvent.getHeader('media-bugname'); if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
if (bugname && this.bugname !== bugname) return; if ('microsoft' === this.vendor) {
const nbest = evt.NBest;
const language_code = evt.PrimaryLanguage?.Language || this.language;
const alternatives = nbest ? nbest.map((n) => {
return {
confidence: n.Confidence,
transcript: n.Display
};
}) :
[
{
transcript: evt.DisplayText
}
];
if (this.vendor === 'ibm') { const newEvent = {
if (evt?.state === 'listening') return; is_final: evt.RecognitionStatus === 'Success',
} channel,
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization'); language_code,
alternatives
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language); };
evt = newEvent;
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription');
if (evt.alternatives[0]?.transcript === '' && !cs.callGone && !this.killed) {
if (['microsoft', 'deepgram'].includes(this.vendor)) {
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening');
}
else {
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, listen again');
this._transcribe(ep);
}
return;
} }
if (this.vendor === 'soniox') { if (evt.alternatives[0].transcript === '' && !cs.callGone && !this.killed) {
/* compile transcripts into one */ this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
this._sonioxTranscripts.push(evt.vendor.finalWords); return this._transcribe(ep);
if (evt.is_final) {
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
this._sonioxTranscripts = [];
}
} }
evt.channel_tag = channel;
if (this.transcriptionHook) { if (this.transcriptionHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
try { this.cs.requestor.request('verb:hook', this.transcriptionHook,
const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, { Object.assign({speech: evt}, this.cs.callInfo), httpHeaders)
...this.cs.callInfo, .catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
...httpHeaders,
speech: evt
});
this.logger.info({json}, 'sent transcriptionHook');
if (json && Array.isArray(json) && !this.parentTask) {
const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.cs.replaceApplication(tasks);
}
}
} catch (err) {
this.logger.info(err, 'TranscribeTask:_onTranscription error');
}
} }
if (this.parentTask) { if (this.parentTask) {
this.parentTask.emit('transcription', evt); this.parentTask.emit('transcription', evt);
@@ -330,64 +302,6 @@ class TaskTranscribe extends Task {
this._timer = null; this._timer = null;
} }
} }
_onDeepgramConnect(_cs, _ep) {
this.logger.debug('TaskTranscribe:_onDeepgramConnect');
}
_onDeepGramConnectFailure(cs, _ep, _channel, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskTranscribe:_onDeepgramConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
vendor: 'deepgram',
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
this.notifyError(`Failed connecting to speech vendor deepgram: ${reason}`);
this.notifyTaskDone();
}
_onIbmConnect(_cs, _ep) {
this.logger.debug('TaskTranscribe:_onIbmConnect');
}
_onIbmConnectFailure(cs, _ep, _channel, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskTranscribe:_onIbmConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
vendor: 'ibm',
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
this.notifyError(`Failed connecting to speech vendor IBM: ${reason}`);
this.notifyTaskDone();
}
_onIbmError(cs, _ep, _channel, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onIbmError');
}
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
} }
module.exports = TaskTranscribe; module.exports = TaskTranscribe;

View File

@@ -1,343 +0,0 @@
const Emitter = require('events');
const {readFile} = require('fs');
const {
GoogleTranscriptionEvents,
AwsTranscriptionEvents,
AzureTranscriptionEvents,
AmdEvents,
AvmdEvents
} = require('./constants');
const bugname = 'amd_bug';
const {VMD_HINTS_FILE} = process.env;
let voicemailHints = [];
const updateHints = async(file, callback) => {
readFile(file, 'utf8', (err, data) => {
if (err) return callback(err);
try {
callback(null, JSON.parse(data));
} catch (err) {
callback(err);
}
});
};
if (VMD_HINTS_FILE) {
updateHints(VMD_HINTS_FILE, (err, hints) => {
if (err) { console.error(err); }
voicemailHints = hints;
/* if successful, update the hints every hour */
setInterval(() => {
updateHints(VMD_HINTS_FILE, (err, hints) => {
if (err) { console.error(err); }
voicemailHints = hints;
});
}, 60000);
});
}
class Amd extends Emitter {
constructor(logger, cs, opts) {
super();
this.logger = logger;
this.vendor = opts.recognizer?.vendor || cs.speechRecognizerVendor;
if ('default' === this.vendor) this.vendor = cs.speechRecognizerVendor;
this.language = opts.recognizer?.language || cs.speechRecognizerLanguage;
if ('default' === this.language) this.language = cs.speechRecognizerLanguage;
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
if (!this.sttCredentials) throw new Error(`No speech credentials found for vendor ${this.vendor}`);
this.thresholdWordCount = opts.thresholdWordCount || 9;
const {normalizeTranscription} = require('./transcription-utils')(logger);
this.normalizeTranscription = normalizeTranscription;
const {
noSpeechTimeoutMs = 5000,
decisionTimeoutMs = 15000,
toneTimeoutMs = 20000,
greetingCompletionTimeoutMs = 2000
} = opts.timers || {};
this.noSpeechTimeoutMs = noSpeechTimeoutMs;
this.decisionTimeoutMs = decisionTimeoutMs;
this.toneTimeoutMs = toneTimeoutMs;
this.greetingCompletionTimeoutMs = greetingCompletionTimeoutMs;
this.beepDetected = false;
}
startDecisionTimer() {
this.decisionTimer = setTimeout(this._onDecisionTimeout.bind(this), this.decisionTimeoutMs);
this.noSpeechTimer = setTimeout(this._onNoSpeechTimeout.bind(this), this.noSpeechTimeoutMs);
this.startToneTimer();
}
stopDecisionTimer() {
this.decisionTimer && clearTimeout(this.decisionTimer);
}
stopNoSpeechTimer() {
this.noSpeechTimer && clearTimeout(this.noSpeechTimer);
}
startToneTimer() {
this.toneTimer = setTimeout(this._onToneTimeout.bind(this), this.toneTimeoutMs);
}
startGreetingCompletionTimer() {
this.greetingCompletionTimer = setTimeout(
this._onGreetingCompletionTimeout.bind(this),
this.beepDetected ? 1000 : this.greetingCompletionTimeoutMs);
}
stopGreetingCompletionTimer() {
this.greetingCompletionTimer && clearTimeout(this.greetingCompletionTimer);
}
restartGreetingCompletionTimer() {
this.stopGreetingCompletionTimer();
this.startGreetingCompletionTimer();
}
stopToneTimer() {
this.toneTimer && clearTimeout(this.toneTimer);
}
stopAllTimers() {
this.stopDecisionTimer();
this.stopNoSpeechTimer();
this.stopToneTimer();
this.stopGreetingCompletionTimer();
}
_onDecisionTimeout() {
this.emit(this.decision = AmdEvents.DecisionTimeout);
this.stopNoSpeechTimer();
}
_onToneTimeout() {
this.emit(AmdEvents.ToneTimeout);
}
_onNoSpeechTimeout() {
this.emit(this.decision = AmdEvents.NoSpeechDetected);
this.stopDecisionTimer();
}
_onGreetingCompletionTimeout() {
this.emit(AmdEvents.MachineStoppedSpeaking);
}
evaluateTranscription(evt) {
if (this.decision) {
/* at this point we are only listening for the machine to stop speaking */
if (this.decision === AmdEvents.MachineDetected) {
this.restartGreetingCompletionTimer();
}
return;
}
this.stopNoSpeechTimer();
this.logger.debug({evt}, 'Amd:evaluateTranscription - raw');
const t = this.normalizeTranscription(evt, this.vendor, this.language);
const hints = voicemailHints[this.language] || [];
this.logger.debug({t}, 'Amd:evaluateTranscription - normalized');
if (Array.isArray(t.alternatives) && t.alternatives.length > 0) {
const wordCount = t.alternatives[0].transcript.split(' ').length;
const final = t.is_final;
const foundHint = hints.find((h) => t.alternatives[0].transcript.includes(h));
if (foundHint) {
/* we detected a common voice mail greeting */
this.logger.debug(`Amd:evaluateTranscription: found hint ${foundHint}`);
this.emit(this.decision = AmdEvents.MachineDetected, {
reason: 'hint',
hint: foundHint,
language: t.language_code
});
}
else if (final && wordCount < this.thresholdWordCount) {
/* a short greeting is typically a human */
this.emit(this.decision = AmdEvents.HumanDetected, {
reason: 'short greeting',
greeting: t.alternatives[0].transcript,
language: t.language_code
});
}
else if (wordCount >= this.thresholdWordCount) {
/* a long greeting is typically a machine */
this.emit(this.decision = AmdEvents.MachineDetected, {
reason: 'long greeting',
greeting: t.alternatives[0].transcript,
language: t.language_code
});
}
if (this.decision) {
this.stopDecisionTimer();
if (this.decision === AmdEvents.MachineDetected) {
/* if we detected a machine, then wait for greeting to end */
this.startGreetingCompletionTimer();
}
}
return this.decision;
}
}
}
module.exports = (logger) => {
const startTranscribing = async(cs, ep, task) => {
const {vendor, language} = ep.amd;
ep.startTranscription({
vendor,
language,
interim: true,
bugname
}).catch((err) => {
const {writeAlerts, AlertType} = cs.srf.locals;
ep.amd = null;
task.emit(AmdEvents.Error, err);
logger.error(err, 'amd:_startTranscribing error');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
vendor: vendor,
detail: err.message
});
}).catch((err) => logger.info({err}, 'Error generating alert for tts failure'));
};
const onEndOfUtterance = (cs, ep, task) => {
logger.debug('amd:onEndOfUtterance');
startTranscribing(cs, ep, task);
};
const onNoSpeechDetected = (cs, ep, task) => {
logger.debug('amd:onNoSpeechDetected');
ep.amd.stopAllTimers();
task.emit(AmdEvents.NoSpeechDetected);
};
const onTranscription = (cs, ep, task, evt, fsEvent) => {
if (fsEvent.getHeader('media-bugname') !== bugname) return;
ep.amd?.evaluateTranscription(evt);
};
const onBeep = (cs, ep, task, evt, fsEvent) => {
logger.debug({evt, fsEvent}, 'onBeep');
const frequency = Math.floor(fsEvent.getHeader('Frequency'));
const variance = Math.floor(fsEvent.getHeader('Frequency-variance'));
task.emit('amd', {type: AmdEvents.ToneDetected, frequency, variance});
if (ep.amd) {
ep.amd.stopToneTimer();
ep.amd.beepDetected = true;
}
ep.execute('avmd_stop').catch((err) => this.logger.info(err, 'Error stopping avmd'));
};
const startAmd = async(cs, ep, task, opts) => {
const amd = ep.amd = new Amd(logger, cs, opts);
const {vendor, language, sttCredentials} = amd;
const sttOpts = {};
const hints = voicemailHints[language] || [];
/* set stt options */
logger.info(`starting amd for vendor ${vendor} and language ${language}`);
if ('google' === vendor) {
sttOpts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(sttCredentials.credentials);
sttOpts.GOOGLE_SPEECH_USE_ENHANCED = true;
sttOpts.GOOGLE_SPEECH_HINTS = hints.join(',');
if (opts.recognizer?.altLanguages) {
sttOpts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
}
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, onEndOfUtterance.bind(null, cs, ep, task));
}
else if (['aws', 'polly'].includes(vendor)) {
Object.assign(sttOpts, {
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
AWS_REGION: sttCredentials.region
});
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
}
else if ('microsoft' === vendor) {
Object.assign(sttOpts, {
'AZURE_SUBSCRIPTION_KEY': sttCredentials.api_key,
'AZURE_REGION': sttCredentials.region
});
sttOpts.AZURE_SPEECH_HINTS = hints.join(',');
if (opts.recognizer?.altLanguages) {
sttOpts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
}
sttOpts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = opts.resolveTimeoutMs || 20000;
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
}
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
amd
.on(AmdEvents.NoSpeechDetected, (evt) => {
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
})
.on(AmdEvents.HumanDetected, (evt) => {
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
})
.on(AmdEvents.MachineDetected, (evt) => {
task.emit('amd', {type: AmdEvents.MachineDetected, ...evt});
})
.on(AmdEvents.DecisionTimeout, (evt) => {
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
})
.on(AmdEvents.ToneTimeout, (evt) => {
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
try {
ep.connected && ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
} catch (err) {
logger.info({err}, 'Error stopping avmd');
}
})
.on(AmdEvents.MachineStoppedSpeaking, () => {
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
});
/* start transcribing, and also listening for beep */
amd.startDecisionTimer();
startTranscribing(cs, ep, task);
ep.addCustomEventListener(AvmdEvents.Beep, onBeep.bind(null, cs, ep, task));
ep.execute('avmd_start').catch((err) => this.logger.info(err, 'Error starting avmd'));
};
const stopAmd = (ep, task) => {
let vendor;
if (ep.amd) {
vendor = ep.amd.vendor;
ep.amd.stopAllTimers();
ep.amd = null;
}
if (ep.connected) {
ep.stopTranscription({vendor, bugname})
.catch((err) => logger.info(err, 'stopAmd: Error stopping transcription'));
task.emit('amd', {type: AmdEvents.Stopped});
ep.execute('avmd_stop').catch((err) => this.logger.info(err, 'Error stopping avmd'));
}
ep.removeCustomEventListener(AvmdEvents.Beep);
};
return {startAmd, stopAmd};
};

View File

@@ -1,7 +1,7 @@
const Emitter = require('events'); const Emitter = require('events');
const bent = require('bent'); const bent = require('bent');
const assert = require('assert'); const assert = require('assert');
const PORT = process.env.AWS_SNS_PORT || 3010; const PORT = process.env.AWS_SNS_PORT || 3001;
const {LifeCycleEvents} = require('./constants'); const {LifeCycleEvents} = require('./constants');
const express = require('express'); const express = require('express');
const app = express(); const app = express();
@@ -21,26 +21,6 @@ class SnsNotifier extends Emitter {
this.logger = logger; this.logger = logger;
} }
_doListen(logger, app, port, resolve) {
return app.listen(port, () => {
this.snsEndpoint = `http://${this.publicIp}:${port}`;
logger.info(`SNS lifecycle server listening on http://localhost:${port}`);
resolve(app);
});
}
_handleErrors(logger, app, resolve, reject, e) {
if (e.code === 'EADDRINUSE' &&
process.env.AWS_SNS_PORT_MAX &&
e.port < process.env.AWS_SNS_PORT_MAX) {
logger.info(`SNS lifecycle server failed to bind port on ${e.port}, will try next port`);
const server = this._doListen(logger, app, ++e.port, resolve);
server.on('error', this._handleErrors.bind(this, logger, app, resolve, reject));
return;
}
reject(e);
}
async _handlePost(req, res) { async _handlePost(req, res) {
try { try {
@@ -104,9 +84,11 @@ class SnsNotifier extends Emitter {
this.logger.debug('SnsNotifier: retrieving instance data'); this.logger.debug('SnsNotifier: retrieving instance data');
this.instanceId = await getString('http://169.254.169.254/latest/meta-data/instance-id'); this.instanceId = await getString('http://169.254.169.254/latest/meta-data/instance-id');
this.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4'); this.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4');
this.snsEndpoint = `http://${this.publicIp}:${PORT}`;
this.logger.info({ this.logger.info({
instanceId: this.instanceId, instanceId: this.instanceId,
publicIp: this.publicIp publicIp: this.publicIp,
snsEndpoint: this.snsEndpoint
}, 'retrieved AWS instance data'); }, 'retrieved AWS instance data');
// start listening // start listening
@@ -118,10 +100,7 @@ class SnsNotifier extends Emitter {
this.logger.error(err, 'burped error'); this.logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message}); res.status(err.status || 500).json({msg: err.message});
}); });
return new Promise((resolve, reject) => { app.listen(PORT);
const server = this._doListen(this.logger, app, PORT, resolve);
server.on('error', this._handleErrors.bind(this, this.logger, app, resolve, reject));
});
} catch (err) { } catch (err) {
this.logger.error({err}, 'Error retrieving AWS instance metadata'); this.logger.error({err}, 'Error retrieving AWS instance metadata');

View File

@@ -20,7 +20,6 @@
"Redirect": "redirect", "Redirect": "redirect",
"RestDial": "rest:dial", "RestDial": "rest:dial",
"SipDecline": "sip:decline", "SipDecline": "sip:decline",
"SipRequest": "sip:request",
"SipRefer": "sip:refer", "SipRefer": "sip:refer",
"SipNotify": "sip:notify", "SipNotify": "sip:notify",
"SipRedirect": "sip:redirect", "SipRedirect": "sip:redirect",
@@ -29,7 +28,6 @@
"Tag": "tag", "Tag": "tag",
"Transcribe": "transcribe" "Transcribe": "transcribe"
}, },
"AllowedSipRecVerbs": ["config", "gather", "transcribe", "listen"],
"CallStatus": { "CallStatus": {
"Trying": "trying", "Trying": "trying",
"Ringing": "ringing", "Ringing": "ringing",
@@ -57,9 +55,6 @@
"StableCall": "stable-call", "StableCall": "stable-call",
"UnansweredCall": "unanswered-call" "UnansweredCall": "unanswered-call"
}, },
"AvmdEvents": {
"Beep": "avmd::beep"
},
"GoogleTranscriptionEvents": { "GoogleTranscriptionEvents": {
"Transcription": "google_transcribe::transcription", "Transcription": "google_transcribe::transcription",
"EndOfUtterance": "google_transcribe::end_of_utterance", "EndOfUtterance": "google_transcribe::end_of_utterance",
@@ -67,35 +62,6 @@
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded", "MaxDurationExceeded": "google_transcribe::max_duration_exceeded",
"VadDetected": "google_transcribe::vad_detected" "VadDetected": "google_transcribe::vad_detected"
}, },
"NuanceTranscriptionEvents": {
"Transcription": "nuance_transcribe::transcription",
"StartOfSpeech": "nuance_transcribe::start_of_speech",
"TranscriptionComplete": "nuance_transcribe::end_of_transcription",
"Error": "nuance_transcribe::error",
"VadDetected": "nuance_transcribe::vad_detected"
},
"NvidiaTranscriptionEvents": {
"Transcription": "nvidia_transcribe::transcription",
"StartOfSpeech": "nvidia_transcribe::start_of_speech",
"TranscriptionComplete": "nvidia_transcribe::end_of_transcription",
"Error": "nvidia_transcribe::error",
"VadDetected": "nvidia_transcribe::vad_detected"
},
"DeepgramTranscriptionEvents": {
"Transcription": "deepgram_transcribe::transcription",
"ConnectFailure": "deepgram_transcribe::connect_failed",
"Connect": "deepgram_transcribe::connect"
},
"SonioxTranscriptionEvents": {
"Transcription": "soniox_transcribe::transcription",
"Error": "soniox_transcribe::error"
},
"IbmTranscriptionEvents": {
"Transcription": "ibm_transcribe::transcription",
"ConnectFailure": "ibm_transcribe::connect_failed",
"Connect": "ibm_transcribe::connect",
"Error": "ibm_transcribe::error"
},
"AwsTranscriptionEvents": { "AwsTranscriptionEvents": {
"Transcription": "aws_transcribe::transcription", "Transcription": "aws_transcribe::transcription",
"EndOfTranscript": "aws_transcribe::end_of_transcript", "EndOfTranscript": "aws_transcribe::end_of_transcript",
@@ -110,12 +76,6 @@
"NoSpeechDetected": "azure_transcribe::no_speech_detected", "NoSpeechDetected": "azure_transcribe::no_speech_detected",
"VadDetected": "azure_transcribe::vad_detected" "VadDetected": "azure_transcribe::vad_detected"
}, },
"JambonzTranscriptionEvents": {
"Transcription": "jambonz_transcribe::transcription",
"ConnectFailure": "jambonz_transcribe::connect_failed",
"Connect": "jambonz_transcribe::connect",
"Error": "jambonz_transcribe::error"
},
"ListenEvents": { "ListenEvents": {
"Connect": "mod_audio_fork::connect", "Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed", "ConnectFailure": "mod_audio_fork::connect_failed",
@@ -157,25 +117,8 @@
"queue:status", "queue:status",
"dial:confirm", "dial:confirm",
"verb:hook", "verb:hook",
"verb:status",
"jambonz:error" "jambonz:error"
], ],
"RecordState": {
"RecordingOn": "recording_on",
"RecordingOff": "recording_off",
"RecordingPaused": "recording_paused"
},
"AmdEvents": {
"NoSpeechDetected": "amd_no_speech_detected",
"HumanDetected": "amd_human_detected",
"MachineDetected": "amd_machine_detected",
"MachineStoppedSpeaking": "amd_machine_stopped_speaking",
"Error": "amd_error",
"DecisionTimeout": "amd_decision_timeout",
"ToneDetected": "amd_tone_detected",
"ToneTimeout": "amd_tone_timeout",
"Stopped": "amd_stopped"
},
"MAX_SIMRINGS": 10, "MAX_SIMRINGS": 10,
"BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)", "BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)",
"FS_UUID_SET_NAME": "fsUUIDs" "FS_UUID_SET_NAME": "fsUUIDs"

View File

@@ -42,9 +42,9 @@ const clearChannels = () => {
}; };
const clearFiles = () => { const clearFiles = () => {
//const {logger} = require('../..'); const {logger} = require('../..');
/*const out = */ execSync('find /tmp -name "*.mp3" -mtime +2 -exec rm {} \\;'); const out = execSync('find /tmp -name "*.mp3" -mtime +2 -exec rm {} \\;');
//logger.debug({out}, 'clearFiles: command output'); logger.debug({out}, 'clearFiles: command output');
}; };

View File

@@ -23,57 +23,22 @@ AND vc.name = ?`;
const speechMapper = (cred) => { const speechMapper = (cred) => {
const {credential, ...obj} = cred; const {credential, ...obj} = cred;
try { if ('google' === obj.vendor) {
if ('google' === obj.vendor) { obj.service_key = decrypt(credential);
obj.service_key = decrypt(credential); }
} else if ('aws' === obj.vendor) {
else if ('aws' === obj.vendor) { const o = JSON.parse(decrypt(credential));
const o = JSON.parse(decrypt(credential)); obj.access_key_id = o.access_key_id;
obj.access_key_id = o.access_key_id; obj.secret_access_key = o.secret_access_key;
obj.secret_access_key = o.secret_access_key; }
obj.aws_region = o.aws_region; else if ('microsoft' === obj.vendor) {
} const o = JSON.parse(decrypt(credential));
else if ('microsoft' === obj.vendor) { obj.api_key = o.api_key;
const o = JSON.parse(decrypt(credential)); obj.region = o.region;
obj.api_key = o.api_key; }
obj.region = o.region; else if ('wellsaid' === obj.vendor) {
obj.use_custom_stt = o.use_custom_stt; const o = JSON.parse(decrypt(credential));
obj.custom_stt_endpoint = o.custom_stt_endpoint; obj.api_key = o.api_key;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret;
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = o.tts_api_key;
obj.tts_region = o.tts_region;
obj.stt_api_key = o.stt_api_key;
obj.stt_region = o.stt_region;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = o.auth_token;
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
} catch (err) {
console.log(err);
} }
return obj; return obj;
}; };
@@ -89,13 +54,32 @@ module.exports = (logger, srf) => {
const [r2] = await pp.query(sqlSpeechCredentials, account_sid); const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
const speech = r2.map(speechMapper); const speech = r2.map(speechMapper);
/* add service provider creds unless we have that vendor at the account level */ /* search at the service provider level if we don't find it at the account level */
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid); const haveGoogle = speech.find((s) => s.vendor === 'google');
r3.forEach((s) => { const haveAws = speech.find((s) => s.vendor === 'aws');
if (!speech.find((s2) => s2.vendor === s.vendor)) { const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
speech.push(speechMapper(s)); const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
if (!haveGoogle || !haveAws || !haveMicrosoft) {
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
if (r3.length) {
if (!haveGoogle) {
const google = r3.find((s) => s.vendor === 'google');
if (google) speech.push(speechMapper(google));
}
if (!haveAws) {
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));
}
} }
}); }
return { return {
...r[0], ...r[0],
@@ -104,7 +88,6 @@ module.exports = (logger, srf) => {
}; };
const updateSpeechCredentialLastUsed = async(speech_credential_sid) => { const updateSpeechCredentialLastUsed = async(speech_credential_sid) => {
if (!speech_credential_sid) return;
const pp = pool.promise(); const pp = pool.promise();
const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?'; const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?';
try { try {

View File

@@ -1,45 +0,0 @@
const express = require('express');
const httpRoutes = require('../http-routes');
const PORT = process.env.HTTP_PORT || 3000;
const doListen = (logger, app, port, resolve) => {
const server = app.listen(port, () => {
const {srf} = app.locals;
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
resolve({server, app});
});
return server;
};
const handleErrors = (logger, app, resolve, reject, e) => {
if (e.code === 'EADDRINUSE' &&
process.env.HTTP_PORT_MAX &&
e.port < process.env.HTTP_PORT_MAX) {
logger.info(`HTTP server failed to bind port on ${e.port}, will try next port`);
const server = doListen(logger, app, ++e.port, resolve);
server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
return;
}
logger.info({err: e, port: PORT}, 'httpListener error');
reject(e);
};
const createHttpListener = (logger, srf) => {
const app = express();
app.locals = {...app.locals, logger, srf};
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use('/', httpRoutes);
app.use((err, _req, res, _next) => {
logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
return new Promise((resolve, reject) => {
const server = doListen(logger, app, PORT, resolve);
server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
});
};
module.exports = createHttpListener;

View File

@@ -1,11 +1,9 @@
const {Client, Pool} = require('undici'); const bent = require('bent');
const parseUrl = require('parse-url'); const parseUrl = require('parse-url');
const assert = require('assert'); const assert = require('assert');
const BaseRequestor = require('./base-requestor'); const BaseRequestor = require('./base-requestor');
const {HookMsgTypes} = require('./constants.json'); const {HookMsgTypes} = require('./constants.json');
const snakeCaseKeys = require('./snakecase-keys'); const snakeCaseKeys = require('./snakecase-keys');
const pools = new Map();
const HTTP_TIMEOUT = 10000;
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64'); const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
@@ -24,47 +22,22 @@ class HttpRequestor extends BaseRequestor {
this.method = hook.method || 'POST'; this.method = hook.method || 'POST';
this.authHeader = basicAuth(hook.username, hook.password); 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(this._isAbsoluteUrl(this.url));
assert(['GET', 'POST'].includes(this.method)); assert(['GET', 'POST'].includes(this.method));
const u = this._parsedUrl = parseUrl(this.url);
if (u.port) this._baseUrl = `${u.protocol}://${u.resource}:${u.port}`;
else this._baseUrl = `${u.protocol}://${u.resource}`;
this._protocol = u.protocol;
this._resource = u.resource;
this._port = u.port;
this._search = u.search;
this._usePools = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
if (this._usePools) {
if (pools.has(this._baseUrl)) {
this.client = pools.get(this._baseUrl);
}
else {
const connections = process.env.HTTP_POOLSIZE ? parseInt(process.env.HTTP_POOLSIZE) : 10;
const pipelining = process.env.HTTP_PIPELINING ? parseInt(process.env.HTTP_PIPELINING) : 1;
const pool = this.client = new Pool(this._baseUrl, {
connections,
pipelining
});
pools.set(this._baseUrl, pool);
this.logger.debug(`HttpRequestor:created pool for ${this._baseUrl}`);
}
}
else {
if (u.port) this.client = new Client(`${u.protocol}://${u.resource}:${u.port}`);
else this.client = new Client(`${u.protocol}://${u.resource}`);
}
} }
get baseUrl() { get baseUrl() {
return this._baseUrl; return this._baseUrl;
} }
close() {
if (!this._usePools && !this.client?.closed) this.client.close();
}
/** /**
* Make an HTTP request. * Make an HTTP request.
* All requests use json bodies. * All requests use json bodies.
@@ -85,73 +58,21 @@ class HttpRequestor extends BaseRequestor {
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null; const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
const url = hook.url || hook; const url = hook.url || hook;
const method = hook.method || 'POST'; const method = hook.method || 'POST';
let buf = '';
assert.ok(url, 'HttpRequestor:request url was not provided'); 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}`); 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(); const startAt = process.hrtime();
/* if we have an absolute url, and it is ws then do a websocket connection */ let buf;
if (this._isAbsoluteUrl(url) && url.startsWith('ws')) {
const WsRequestor = require('./ws-requestor');
this.logger.debug({hook}, 'HttpRequestor: switching to websocket connection');
const h = typeof hook === 'object' ? hook : {url: hook};
const requestor = new WsRequestor(this.logger, this.account_sid, h, this.secret);
if (type === 'session:redirect') {
this.close();
this.emit('handover', requestor);
}
return requestor.request('session:new', hook, params, httpHeaders);
}
let newClient;
try { try {
let client, path, query;
if (this._isRelativeUrl(url)) {
client = this.client;
path = url;
}
else {
const u = parseUrl(url);
if (u.resource === this._resource && u.port === this._port && u.protocol === this._protocol) {
client = this.client;
path = u.pathname;
query = u.query;
}
else {
if (u.port) client = newClient = new Client(`${u.protocol}://${u.resource}:${u.port}`);
else client = newClient = new Client(`${u.protocol}://${u.resource}`);
path = u.pathname;
query = u.query;
}
}
const sigHeader = this._generateSigHeader(payload, this.secret); const sigHeader = this._generateSigHeader(payload, this.secret);
const hdrs = { const headers = {...sigHeader, ...this.authHeader, ...httpHeaders};
...sigHeader, this.logger.debug({url, headers}, 'send webhook');
...this.authHeader, buf = this._isRelativeUrl(url) ?
...httpHeaders, await this.post(url, payload, headers) :
...('POST' === method && {'Content-Type': 'application/json'}) await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
};
const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url;
this.logger.debug({url, absUrl, hdrs}, 'send webhook');
const {statusCode, headers, body} = await client.request({
path,
query,
method,
headers: hdrs,
...('POST' === method && {body: JSON.stringify(payload)}),
timeout: HTTP_TIMEOUT,
followRedirects: false
});
if (![200, 202, 204].includes(statusCode)) {
const err = new Error();
err.statusCode = statusCode;
throw err;
}
if (headers['content-type']?.includes('application/json')) {
buf = await body.json();
}
if (newClient) newClient.close();
} catch (err) { } catch (err) {
if (err.statusCode) { if (err.statusCode) {
this.logger.info({baseUrl: this.baseUrl, url}, this.logger.info({baseUrl: this.baseUrl, url},
@@ -173,15 +94,20 @@ class HttpRequestor extends BaseRequestor {
} }
this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert')); this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
if (newClient) newClient.close();
throw err; throw err;
} }
const rtt = this._roundTrip(startAt); const rtt = this._roundTrip(startAt);
if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']); if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']);
if (buf && Array.isArray(buf)) { if (buf && buf.toString().length > 0) {
this.logger.info({response: buf}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`); try {
return buf; 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()}'`);
}
} }
} }
} }

View File

@@ -138,6 +138,7 @@ function installSrfLocals(srf, logger) {
retrieveCall, retrieveCall,
listCalls, listCalls,
deleteCall, deleteCall,
synthAudio,
createHash, createHash,
retrieveHash, retrieveHash,
deleteKey, deleteKey,
@@ -150,20 +151,12 @@ function installSrfLocals(srf, logger) {
pushBack, pushBack,
popFront, popFront,
removeFromList, removeFromList,
getListPosition,
lengthOfList, lengthOfList,
getListPosition
} = require('@jambonz/realtimedb-helpers')({ } = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST, host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379 port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger, tracer); }, logger, tracer);
const {
synthAudio,
getNuanceAccessToken,
getIbmAccessToken,
} = require('@jambonz/speech-utils')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger, tracer);
const { const {
writeAlerts, writeAlerts,
AlertType AlertType
@@ -211,9 +204,7 @@ function installSrfLocals(srf, logger) {
popFront, popFront,
removeFromList, removeFromList,
lengthOfList, lengthOfList,
getListPosition, getListPosition
getNuanceAccessToken,
getIbmAccessToken
}, },
parentLogger: logger, parentLogger: logger,
getSBC, getSBC,

View File

@@ -0,0 +1,31 @@
function normalizeJambones(logger, obj) {
if (!Array.isArray(obj)) throw new Error('malformed jambonz payload: must be array');
const document = [];
for (const tdata of obj) {
if (typeof tdata !== 'object') throw new Error('malformed jambonz payload: must be array of objects');
if ('verb' in tdata) {
// {verb: 'say', text: 'foo..bar'..}
const name = tdata.verb;
const o = {};
Object.keys(tdata)
.filter((k) => k !== 'verb')
.forEach((k) => o[k] = tdata[k]);
const o2 = {};
o2[name] = o;
document.push(o2);
}
else if (Object.keys(tdata).length === 1) {
// {'say': {..}}
document.push(tdata);
}
else {
logger.info(tdata, 'malformed jambonz payload: missing verb property');
throw new Error('malformed jambonz payload: missing verb property');
}
}
logger.debug({document}, `normalizeJambones: returning document with ${document.length} tasks`);
return document;
}
module.exports = normalizeJambones;

View File

@@ -4,7 +4,7 @@ const SipError = require('drachtio-srf').SipError;
const {TaskPreconditions, CallDirection} = require('../utils/constants'); const {TaskPreconditions, CallDirection} = require('../utils/constants');
const CallInfo = require('../session/call-info'); const CallInfo = require('../session/call-info');
const assert = require('assert'); const assert = require('assert');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const makeTask = require('../tasks/make_task'); const makeTask = require('../tasks/make_task');
const ConfirmCallSession = require('../session/confirm-call-session'); const ConfirmCallSession = require('../session/confirm-call-session');
const AdultingCallSession = require('../session/adulting-call-session'); const AdultingCallSession = require('../session/adulting-call-session');
@@ -12,7 +12,7 @@ const deepcopy = require('deepcopy');
const moment = require('moment'); const moment = require('moment');
const stripCodecs = require('./strip-ancillary-codecs'); const stripCodecs = require('./strip-ancillary-codecs');
const RootSpan = require('./call-tracer'); const RootSpan = require('./call-tracer');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
class SingleDialer extends Emitter { class SingleDialer extends Emitter {
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) { constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
@@ -21,7 +21,6 @@ class SingleDialer extends Emitter {
this.logger = logger; this.logger = logger;
this.target = target; this.target = target;
this.from = target.from || {};
this.sbcAddress = sbcAddress; this.sbcAddress = sbcAddress;
this.opts = opts; this.opts = opts;
this.application = application; this.application = application;
@@ -67,11 +66,8 @@ class SingleDialer extends Emitter {
opts.headers = { opts.headers = {
...opts.headers, ...opts.headers,
...(this.target.headers || {}), ...(this.target.headers || {}),
...(this.from.user && {'X-Preferred-From-User': this.from.user}),
...(this.from.host && {'X-Preferred-From-Host': this.from.host}),
'X-Jambonz-Routing': this.target.type, 'X-Jambonz-Routing': this.target.type,
'X-Call-Sid': this.callSid, 'X-Call-Sid': this.callSid
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
}; };
if (srf.locals.fsUUID) { if (srf.locals.fsUUID) {
opts.headers = { opts.headers = {
@@ -412,7 +408,7 @@ class SingleDialer extends Emitter {
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason); this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
if (typeof duration === 'number') this.callInfo.duration = duration; if (typeof duration === 'number') this.callInfo.duration = duration;
try { try {
this.notifier.request('call:status', this.application.call_status_hook, this.callInfo.toJSON()); this.requestor.request('call:status', this.application.call_status_hook, this.callInfo.toJSON());
} catch (err) { } catch (err) {
this.logger.info(err, `SingleDialer:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`); this.logger.info(err, `SingleDialer:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
} }

View File

@@ -1,7 +1,42 @@
const bent = require('bent');
const parseUrl = require('parse-url');
const assert = require('assert'); const assert = require('assert');
const snakeCaseKeys = require('./snakecase-keys');
const crypto = require('crypto');
const timeSeries = require('@jambonz/time-series'); const timeSeries = require('@jambonz/time-series');
let alerter ; let alerter ;
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
function computeSignature(payload, timestamp, secret) {
assert(secret);
const data = `${timestamp}.${JSON.stringify(payload)}`;
return crypto
.createHmac('sha256', secret)
.update(data, 'utf8')
.digest('hex');
}
function generateSigHeader(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signature = computeSignature(payload, timestamp, secret);
const scheme = 'v1';
return {
'Jambonz-Signature': `t=${timestamp},${scheme}=${signature}`
};
}
function basicAuth(username, password) {
if (!username || !password) return {};
const creds = `${username}:${password || ''}`;
const header = `Basic ${toBase64(creds)}`;
return {Authorization: header};
}
function isRelativeUrl(u) {
return typeof u === 'string' && u.startsWith('/');
}
function isAbsoluteUrl(u) { function isAbsoluteUrl(u) {
return typeof u === 'string' && return typeof u === 'string' &&
u.startsWith('https://') || u.startsWith('http://'); u.startsWith('https://') || u.startsWith('http://');
@@ -14,6 +49,14 @@ class Requestor {
this.logger = logger; this.logger = logger;
this.url = hook.url; this.url = hook.url;
this.method = hook.method || 'POST'; 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);
this.username = hook.username; this.username = hook.username;
this.password = hook.password; this.password = hook.password;
@@ -35,15 +78,72 @@ class Requestor {
} }
} }
get Alerter() { get baseUrl() {
if (!alerter) { return this._baseUrl;
alerter = timeSeries(this.logger, { }
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50, /**
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20 * 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(hook, params) {
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
const url = hook.url || hook;
const method = hook.method || 'POST';
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}`);
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');
buf = isRelativeUrl(url) ?
await this.post(url, payload, headers) :
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
} catch (err) {
this.logger.error({err, secret: this.secret, baseUrl: this.baseUrl, url, statusCode: err.statusCode},
`web callback returned unexpected error code ${err.statusCode}`);
let opts = {account_sid: this.account_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
}
else if (err.name === 'StatusError') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_STATUS_FAILURE, url, status: err.statusCode};
}
else {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url, detail: err.message};
}
alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
throw err;
}
const diff = process.hrtime(startAt);
const time = diff[0] * 1e3 + diff[1] * 1e-6;
const rtt = time.toFixed(0);
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}, `Requestor:request ${method} ${url} succeeded in ${rtt}ms`);
return json;
}
catch (err) {
//this.logger.debug({err, url, method}, `Requestor:request returned non-JSON content: '${buf.toString()}'`);
}
} }
return alerter;
} }
} }

View File

@@ -1,5 +1,5 @@
const assert = require('assert'); const assert = require('assert');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants'); const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants');
const Emitter = require('events'); const Emitter = require('events');
const debug = require('debug')('jambonz:feature-server'); const debug = require('debug')('jambonz:feature-server');
@@ -75,10 +75,6 @@ module.exports = (logger) => {
} }
})(); })();
} }
else if (process.env.K8S) {
lifecycleEmitter.scaleIn = () => process.exit(0);
}
async function pingProxies(srf) { async function pingProxies(srf) {
if (process.env.NODE_ENV === 'test') return; if (process.env.NODE_ENV === 'test') return;
@@ -108,14 +104,8 @@ module.exports = (logger) => {
const {srf} = require('../..'); const {srf} = require('../..');
const {addToSet} = srf.locals.dbHelpers; const {addToSet} = srf.locals.dbHelpers;
const uuid = srf.locals.fsUUID = uuidv4(); const uuid = srf.locals.fsUUID = uuidv4();
addToSet(FS_UUID_SET_NAME, uuid)
/* in case redis is restarted, re-insert our key every so often */ .catch((err) => logger.info({err}, `Error adding ${uuid} to set ${FS_UUID_SET_NAME}`));
setInterval(() => {
// eslint-disable-next-line max-len
addToSet(FS_UUID_SET_NAME, uuid).catch((err) => logger.info({err}, `Error adding ${uuid} to set ${FS_UUID_SET_NAME}`));
}, 30000);
// eslint-disable-next-line max-len
addToSet(FS_UUID_SET_NAME, uuid).catch((err) => logger.info({err}, `Error adding ${uuid} to set ${FS_UUID_SET_NAME}`));
}); });
} }
else { else {

View File

@@ -1,248 +0,0 @@
const xmlParser = require('xml2js').parseString;
const uuidv4 = require('uuid-random');
const parseUri = require('drachtio-srf').parseUri;
const transform = require('sdp-transform');
const debug = require('debug')('jambonz:feature-server');
const parseCallData = (prefix, obj) => {
const ret = {};
const group = obj[`${prefix}group`];
if (group) {
const key = Object.keys(group[0]).find((k) => /:?callData$/.test(k));
//const o = _.find(group[0], (value, key) => /:?callData$/.test(key));
if (key) {
//const callData = o[0];
const callData = group[0][key];
for (const key of Object.keys(callData)) {
if (['fromhdr', 'tohdr', 'callid'].includes(key)) ret[key] = callData[key][0];
}
}
}
debug('parseCallData', prefix, obj, ret);
return ret;
};
/**
* parse a SIPREC multiparty body
* @param {object} opts - options
* @return {Promise}
*/
const parseSiprecPayload = (req, logger) => {
const opts = {};
return new Promise((resolve, reject) => {
let sdp, meta ;
for (let i = 0; i < req.payload.length; i++) {
switch (req.payload[i].type) {
case 'application/sdp':
sdp = req.payload[i].content ;
break ;
case 'application/rs-metadata+xml':
case 'application/rs-metadata':
meta = opts.xml = req.payload[i].content ;
break ;
default:
break ;
}
}
if (!sdp || !meta) {
logger.info({payload: req.payload}, 'invalid SIPREC payload');
return reject(new Error('expected multipart SIPREC body'));
}
xmlParser(meta, (err, result) => {
if (err) { throw err; }
opts.recordingData = result ;
opts.sessionId = uuidv4() ;
const arr = /^([^]+)(m=[^]+?)(m=[^]+?)$/.exec(sdp) ;
opts.sdp1 = `${arr[1]}${arr[2]}` ;
opts.sdp2 = `${arr[1]}${arr[3]}\r\n` ;
try {
if (typeof result === 'object' && Object.keys(result).length === 1) {
const key = Object.keys(result)[0] ;
const arr = /^(.*:)recording/.exec(key) ;
const prefix = !arr ? '' : (arr[1]) ;
const obj = opts.recordingData[`${prefix}recording`];
// 1. collect participant data
const participants = {} ;
obj[`${prefix}participant`].forEach((p) => {
const partDetails = {} ;
participants[p.$.participant_id] = partDetails;
if ((`${prefix}nameID` in p) && Array.isArray(p[`${prefix}nameID`])) {
partDetails.aor = p[`${prefix}nameID`][0].$.aor;
if ('name' in p[`${prefix}nameID`][0] && Array.isArray(p[`${prefix}nameID`][0].name)) {
const name = p[`${prefix}nameID`][0].name[0];
if (typeof name === 'string') partDetails.name = name ;
else if (typeof name === 'object') partDetails.name = name._ ;
}
}
});
// 2. find the associated streams for each participant
if (`${prefix}participantstreamassoc` in obj) {
obj[`${prefix}participantstreamassoc`].forEach((ps) => {
const part = participants[ps.$.participant_id];
if (part) {
part.send = ps[`${prefix}send`][0];
part.recv = ps[`${prefix}recv`][0];
}
});
}
// 3. Retrieve stream data
opts.caller = {} ;
opts.callee = {} ;
obj[`${prefix}stream`].forEach((s) => {
const streamId = s.$.stream_id;
let sender;
for (const [k, v] of Object.entries(participants)) {
if (v.send === streamId) {
sender = k;
break;
}
}
//const sender = _.find(participants, { 'send': streamId});
if (!sender) return;
sender.label = s[`${prefix}label`][0];
if (-1 !== ['1', 'a_leg', 'inbound'].indexOf(sender.label)) {
opts.caller.aor = sender.aor ;
if (sender.name) opts.caller.name = sender.name;
}
else {
opts.callee.aor = sender.aor ;
if (sender.name) opts.callee.name = sender.name;
}
});
// if we dont have a participantstreamassoc then assume the first participant is the caller
if (!opts.caller.aor && !opts.callee.aor) {
let i = 0;
for (const part in participants) {
const p = participants[part];
if (0 === i && p.aor) {
opts.caller.aor = p.aor;
opts.caller.name = p.name;
}
else if (1 === i && p.aor) {
opts.callee.aor = p.aor;
opts.callee.name = p.name;
}
i++;
}
}
// now for Sonus (at least) we get the original from, to and call-id headers in a <callData/> element
// if so, this should take preference
const callData = parseCallData(prefix, obj);
if (callData) {
debug(`callData: ${JSON.stringify(callData)}`);
opts.originalCallId = callData.callid;
// caller
let r1 = /^(.*)(<sip.*)$/.exec(callData.fromhdr);
if (r1) {
const arr = /<(.*)>/.exec(r1[2]);
if (arr) {
const uri = parseUri(arr[1]);
const user = uri.user || 'anonymous';
opts.caller.aor = `sip:${user}@${uri.host}`;
}
const dname = r1[1].trim();
const arr2 = /"(.*)"/.exec(dname);
if (arr2) opts.caller.name = arr2[1];
else opts.caller.name = dname;
}
// callee
r1 = /^(.*)(<sip.*)$/.exec(callData.tohdr);
if (r1) {
const arr = /<(.*)>/.exec(r1[2]);
if (arr) {
const uri = parseUri(arr[1]);
opts.callee.aor = `sip:${uri.user}@${uri.host}`;
}
const dname = r1[1].trim();
const arr2 = /"(.*)"/.exec(dname);
if (arr2) opts.callee.name = arr2[1];
else opts.callee.name = dname;
}
debug(`opts.caller from callData: ${JSON.stringify(opts.caller)}`);
debug(`opts.callee from callData: ${JSON.stringify(opts.callee)}`);
}
if (opts.caller.aor && 0 !== opts.caller.aor.indexOf('sip:')) {
opts.caller.aor = 'sip:' + opts.caller.aor;
}
if (opts.callee.aor && 0 !== opts.callee.aor.indexOf('sip:')) {
opts.callee.aor = 'sip:' + opts.callee.aor;
}
if (opts.caller.aor) {
const uri = parseUri(opts.caller.aor);
opts.caller.number = uri.user;
}
if (opts.callee.aor) {
const uri = parseUri(opts.callee.aor);
opts.callee.number = uri.user;
}
opts.recordingSessionId = opts.recordingData[`${prefix}recording`][`${prefix}session`][0].$.session_id;
}
}
catch (err) {
reject(err);
}
debug(opts, 'payload parser results');
resolve(opts) ;
}) ;
}) ;
};
const createSipRecPayload = (sdp1, sdp2, logger) => {
const sdpObj = [];
sdpObj.push(transform.parse(sdp1));
sdpObj.push(transform.parse(sdp2));
//const arr1 = /^([^]+)(c=[^]+)t=[^]+(m=[^]+?)(a=[^]+)$/.exec(sdp1) ;
//const arr2 = /^([^]+)(c=[^]+)t=[^]+(m=[^]+?)(a=[^]+)$/.exec(sdp2) ;
debug(`sdp1: ${sdp1}`);
debug(`objSdp[0]: ${JSON.stringify(sdpObj[0])}`);
debug(`sdp2: ${sdp2}`);
debug(`objSdp[1]: ${JSON.stringify(sdpObj[1])}`);
if (!sdpObj[0] || !sdpObj[0].media.length) {
throw new Error(`Error parsing sdp1 into component parts: ${sdp1}`);
}
else if (!sdpObj[1] || !sdpObj[1].media.length) {
throw new Error(`Error parsing sdp2 into component parts: ${sdp2}`);
}
if (!sdpObj[0].media[0].label) sdpObj[0].media[0].label = 1;
if (!sdpObj[1].media[0].label) sdpObj[1].media[0].label = 2;
//const aLabel = sdp1.includes('a=label:') ? '' : 'a=label:1\r\n';
//const bLabel = sdp2.includes('a=label:') ? '' : 'a=label:2\r\n';
sdpObj[0].media = sdpObj[0].media.concat(sdpObj[1].media);
const combinedSdp = transform.write(sdpObj[0])
.replace(/a=sendonly\r\n/g, '')
.replace(/a=direction:both\r\n/g, '');
debug(`combined ${combinedSdp}`);
/*
const combinedSdp = `${arr1[1]}t=0 0\r\n${arr1[2]}${arr1[3]}${arr1[4]}${aLabel}${arr2[3]}${arr2[4]}${bLabel}`
.replace(/a=sendonly\r\n/g, '')
.replace(/a=direction:both\r\n/g, '');
*/
return combinedSdp;
};
module.exports = { parseSiprecPayload, createSipRecPayload } ;

View File

@@ -1,697 +0,0 @@
const {
TaskName,
AzureTranscriptionEvents,
GoogleTranscriptionEvents,
AwsTranscriptionEvents,
NuanceTranscriptionEvents,
DeepgramTranscriptionEvents,
SonioxTranscriptionEvents,
NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('./constants');
const stickyVars = {
google: [
'GOOGLE_SPEECH_HINTS',
'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL',
'GOOGLE_SPEECH_PROFANITY_FILTER',
'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS',
'GOOGLE_SPEECH_SINGLE_UTTERANCE',
'GOOGLE_SPEECH_SPEAKER_DIARIZATION',
'GOOGLE_SPEECH_USE_ENHANCED',
'GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
'GOOGLE_SPEECH_METADATA_INTERACTION_TYPE',
'GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE'
],
microsoft: [
'AZURE_SPEECH_HINTS',
'AZURE_SERVICE_ENDPOINT_ID',
'AZURE_REQUEST_SNR',
'AZURE_PROFANITY_OPTION',
'AZURE_SERVICE_ENDPOINT',
'AZURE_INITIAL_SPEECH_TIMEOUT_MS',
'AZURE_USE_OUTPUT_FORMAT_DETAILED',
],
deepgram: [
'DEEPGRAM_SPEECH_KEYWORDS',
'DEEPGRAM_API_KEY',
'DEEPGRAM_SPEECH_TIER',
'DEEPGRAM_SPEECH_MODEL',
'DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
'DEEPGRAM_SPEECH_PROFANITY_FILTER',
'DEEPGRAM_SPEECH_REDACT',
'DEEPGRAM_SPEECH_DIARIZE',
'DEEPGRAM_SPEECH_NER',
'DEEPGRAM_SPEECH_ALTERNATIVES',
'DEEPGRAM_SPEECH_NUMERALS',
'DEEPGRAM_SPEECH_SEARCH',
'DEEPGRAM_SPEECH_REPLACE',
'DEEPGRAM_SPEECH_ENDPOINTING',
'DEEPGRAM_SPEECH_VAD_TURNOFF',
'DEEPGRAM_SPEECH_TAG'
],
aws: [
'AWS_VOCABULARY_NAME',
'AWS_VOCABULARY_FILTER_METHOD',
'AWS_VOCABULARY_FILTER_NAME'
],
nuance: [
'NUANCE_ACCESS_TOKEN',
'NUANCE_KRYPTON_ENDPOINT',
'NUANCE_TOPIC',
'NUANCE_UTTERANCE_DETECTION_MODE',
'NUANCE_FILTER_PROFANITY',
'NUANCE_INCLUDE_TOKENIZATION',
'NUANCE_DISCARD_SPEAKER_ADAPTATION',
'NUANCE_SUPPRESS_CALL_RECORDING',
'NUANCE_MASK_LOAD_FAILURES',
'NUANCE_SUPPRESS_INITIAL_CAPITALIZATION',
'NUANCE_ALLOW_ZERO_BASE_LM_WEIGHT',
'NUANCE_FILTER_WAKEUP_WORD',
'NUANCE_NO_INPUT_TIMEOUT_MS',
'NUANCE_RECOGNITION_TIMEOUT_MS',
'NUANCE_UTTERANCE_END_SILENCE_MS',
'NUANCE_MAX_HYPOTHESES',
'NUANCE_SPEECH_DOMAIN',
'NUANCE_FORMATTING',
'NUANCE_RESOURCES'
],
ibm: [
'IBM_ACCESS_TOKEN',
'IBM_SPEECH_REGION',
'IBM_SPEECH_INSTANCE_ID',
'IBM_SPEECH_MODEL',
'IBM_SPEECH_LANGUAGE_CUSTOMIZATION_ID',
'IBM_SPEECH_ACOUSTIC_CUSTOMIZATION_ID',
'IBM_SPEECH_BASE_MODEL_VERSION',
'IBM_SPEECH_WATSON_METADATA',
'IBM_SPEECH_WATSON_LEARNING_OPT_OUT'
],
nvidia: [
'NVIDIA_HINTS'
],
soniox: [
'SONIOX_PROFANITY_FILTER',
'SONIOX_MODEL'
]
};
const compileSonioxTranscripts = (finalWordChunks, channel, language) => {
const words = finalWordChunks.flat();
const transcript = words.reduce((acc, word) => {
if (word.text === '<end>') return acc;
if ([',', '.', '?', '!'].includes(word.text)) return `${acc}${word.text}`;
return `${acc} ${word.text}`;
}, '').trim();
const realWords = words.filter((word) => ![',.!?;'].includes(word.text) && word.text !== '<end>');
const confidence = realWords.reduce((acc, word) => acc + word.confidence, 0) / realWords.length;
const alternatives = [{transcript, confidence}];
return {
language_code: language,
channel_tag: channel,
is_final: true,
alternatives,
vendor: {
name: 'soniox',
evt: words
}
};
};
const normalizeSoniox = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
/* an <end> token indicates the end of an utterance */
const endTokenPos = evt.words.map((w) => w.text).indexOf('<end>');
const endpointReached = endTokenPos !== -1;
const words = endpointReached ? evt.words.slice(0, endTokenPos) : evt.words;
/* note: we can safely ignore words after the <end> token as they will be returned again */
const finalWords = words.filter((word) => word.is_final);
const nonFinalWords = words.filter((word) => !word.is_final);
const is_final = endpointReached && finalWords.length > 0;
const transcript = words.reduce((acc, word) => {
if ([',', '.', '?', '!'].includes(word.text)) return `${acc}${word.text}`;
else return `${acc} ${word.text}`;
}, '').trim();
const realWords = words.filter((word) => ![',.!?;'].includes(word.text) && word.text !== '<end>');
const confidence = realWords.reduce((acc, word) => acc + word.confidence, 0) / realWords.length;
const alternatives = [{transcript, confidence}];
return {
language_code: language,
channel_tag: channel,
is_final,
alternatives,
vendor: {
name: 'soniox',
endpointReached,
evt: copy,
finalWords,
nonFinalWords
}
};
};
const normalizeDeepgram = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
const alternatives = (evt.channel?.alternatives || [])
.map((alt) => ({
confidence: alt.confidence,
transcript: alt.transcript,
}));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [alternatives[0]],
vendor: {
name: 'deepgram',
evt: copy
}
};
};
const normalizeNvidia = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
const alternatives = (evt.alternatives || [])
.map((alt) => ({
confidence: alt.confidence,
transcript: alt.transcript,
}));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives,
vendor: {
name: 'nvidia',
evt: copy
}
};
};
const normalizeIbm = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
//const idx = evt.result_index;
const result = evt.results[0];
return {
language_code: language,
channel_tag: channel,
is_final: result.final,
alternatives: result.alternatives,
vendor: {
name: 'ibm',
evt: copy
}
};
};
const normalizeGoogle = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]],
vendor: {
name: 'google',
evt: copy
}
};
};
const normalizeCustom = (evt, channel, language) => {
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]]
};
};
const normalizeNuance = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]],
vendor: {
name: 'nuance',
evt: copy
}
};
};
const normalizeMicrosoft = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
const nbest = evt.NBest;
const language_code = evt.PrimaryLanguage?.Language || language;
const alternatives = nbest ? nbest.map((n) => {
return {
confidence: n.Confidence,
transcript: n.Display
};
}) :
[
{
transcript: evt.DisplayText || evt.Text
}
];
return {
language_code,
channel_tag: channel,
is_final: evt.RecognitionStatus === 'Success',
alternatives: [alternatives[0]],
vendor: {
name: 'microsoft',
evt: copy
}
};
};
const normalizeAws = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt[0].is_final,
alternatives: evt[0].alternatives,
vendor: {
name: 'aws',
evt: copy
}
};
};
module.exports = (logger) => {
const normalizeTranscription = (evt, vendor, channel, language) => {
//logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription');
switch (vendor) {
case 'deepgram':
return normalizeDeepgram(evt, channel, language);
case 'microsoft':
return normalizeMicrosoft(evt, channel, language);
case 'google':
return normalizeGoogle(evt, channel, language);
case 'aws':
return normalizeAws(evt, channel, language);
case 'nuance':
return normalizeNuance(evt, channel, language);
case 'ibm':
return normalizeIbm(evt, channel, language);
case 'nvidia':
return normalizeNvidia(evt, channel, language);
case 'soniox':
return normalizeSoniox(evt, channel, language);
default:
if (vendor.startsWith('custom:')) {
return normalizeCustom(evt, channel, language);
}
logger.error(`Unknown vendor ${vendor}`);
return evt;
}
};
const setChannelVarsForStt = (task, sttCredentials, rOpts = {}) => {
let opts = {};
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
const vad = {enable, voiceMs, mode};
const vendor = rOpts.vendor;
/* voice activity detection works across vendors */
opts = {
...opts,
...(vad.enable && {START_RECOGNIZING_ON_VAD: 1}),
...(vad.enable && vad.voiceMs && {RECOGNIZER_VAD_VOICE_MS: vad.voiceMs}),
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
};
if ('google' === vendor) {
opts = {
...opts,
...(sttCredentials &&
{GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
...(rOpts.enhancedModel &&
{GOOGLE_SPEECH_USE_ENHANCED: 1}),
...(rOpts.separateRecognitionPerChannel &&
{GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
...(rOpts.profanityFilter &&
{GOOGLE_SPEECH_PROFANITY_FILTER: 1}),
...(rOpts.punctuation &&
{GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1}),
...(rOpts.words &&
{GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 1}),
...((rOpts.singleUtterance || task.name === TaskName.Gather) &&
{GOOGLE_SPEECH_SINGLE_UTTERANCE: 1}),
...(rOpts.diarization &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION: 1}),
...(rOpts.diarization && rOpts.diarizationMinSpeakers > 0 &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT: rOpts.diarizationMinSpeakers}),
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
...(rOpts.enhancedModel === false &&
{GOOGLE_SPEECH_USE_ENHANCED: 0}),
...(rOpts.separateRecognitionPerChannel === false &&
{GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 0}),
...(rOpts.profanityFilter === false &&
{GOOGLE_SPEECH_PROFANITY_FILTER: 0}),
...(rOpts.punctuation === false &&
{GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 0}),
...(rOpts.words == false &&
{GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 0}),
...((rOpts.singleUtterance === false || task.name === TaskName.Transcribe) &&
{GOOGLE_SPEECH_SINGLE_UTTERANCE: 0}),
...(rOpts.diarization === false &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' &&
{GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
...(rOpts.altLanguages.length > 0 &&
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: rOpts.altLanguages.join(',')}),
...(rOpts.interactionType &&
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
...{GOOGLE_SPEECH_MODEL: rOpts.model || (task.name === TaskName.Gather ? 'latest_short' : 'phone_call')},
...(rOpts.naicsCode > 0 &&
{GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
};
}
else if (['aws', 'polly'].includes(vendor)) {
opts = {
...opts,
...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}),
...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}),
...(sttCredentials && {
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
AWS_REGION: sttCredentials.region
}),
};
}
else if ('microsoft' === vendor) {
opts = {
...opts,
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.trim()).join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}),
...(rOpts.altLanguages && rOpts.altLanguages.length > 0 &&
{AZURE_SERVICE_ENDPOINT_ID: rOpts.sttCredentials}),
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
...(rOpts.initialSpeechTimeoutMs > 0 &&
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
...(sttCredentials && {
AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key,
AZURE_REGION: sttCredentials.region,
}),
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint &&
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint})
};
}
else if ('nuance' === vendor) {
/**
* Note: all nuance options are in recognizer.nuanceOptions, should migrate
* other vendor settings to similar nested structure
*/
const {nuanceOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.access_token) &&
{NUANCE_ACCESS_TOKEN: sttCredentials.access_token},
...(sttCredentials.krypton_endpoint) &&
{NUANCE_KRYPTON_ENDPOINT: sttCredentials.krypton_endpoint},
...(nuanceOptions.topic) &&
{NUANCE_TOPIC: nuanceOptions.topic},
...(nuanceOptions.utteranceDetectionMode) &&
{NUANCE_UTTERANCE_DETECTION_MODE: nuanceOptions.utteranceDetectionMode},
...(nuanceOptions.punctuation || rOpts.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
...(nuanceOptions.profanityFilter) &&
{NUANCE_FILTER_PROFANITY: nuanceOptions.profanityFilter},
...(nuanceOptions.includeTokenization) &&
{NUANCE_INCLUDE_TOKENIZATION: nuanceOptions.includeTokenization},
...(nuanceOptions.discardSpeakerAdaptation) &&
{NUANCE_DISCARD_SPEAKER_ADAPTATION: nuanceOptions.discardSpeakerAdaptation},
...(nuanceOptions.suppressCallRecording) &&
{NUANCE_SUPPRESS_CALL_RECORDING: nuanceOptions.suppressCallRecording},
...(nuanceOptions.maskLoadFailures) &&
{NUANCE_MASK_LOAD_FAILURES: nuanceOptions.maskLoadFailures},
...(nuanceOptions.suppressInitialCapitalization) &&
{NUANCE_SUPPRESS_INITIAL_CAPITALIZATION: nuanceOptions.suppressInitialCapitalization},
...(nuanceOptions.allowZeroBaseLmWeight)
&& {NUANCE_ALLOW_ZERO_BASE_LM_WEIGHT: nuanceOptions.allowZeroBaseLmWeight},
...(nuanceOptions.filterWakeupWord) &&
{NUANCE_FILTER_WAKEUP_WORD: nuanceOptions.filterWakeupWord},
...(nuanceOptions.resultType) &&
{NUANCE_RESULT_TYPE: nuanceOptions.resultType || rOpts.interim ? 'partial' : 'final'},
...(nuanceOptions.noInputTimeoutMs) &&
{NUANCE_NO_INPUT_TIMEOUT_MS: nuanceOptions.noInputTimeoutMs},
...(nuanceOptions.recognitionTimeoutMs) &&
{NUANCE_RECOGNITION_TIMEOUT_MS: nuanceOptions.recognitionTimeoutMs},
...(nuanceOptions.utteranceEndSilenceMs) &&
{NUANCE_UTTERANCE_END_SILENCE_MS: nuanceOptions.utteranceEndSilenceMs},
...(nuanceOptions.maxHypotheses) &&
{NUANCE_MAX_HYPOTHESES: nuanceOptions.maxHypotheses},
...(nuanceOptions.speechDomain) &&
{NUANCE_SPEECH_DOMAIN: nuanceOptions.speechDomain},
...(nuanceOptions.formatting) &&
{NUANCE_FORMATTING: nuanceOptions.formatting},
...(nuanceOptions.resources) &&
{NUANCE_RESOURCES: JSON.stringify(nuanceOptions.resources)},
};
}
else if ('deepgram' === vendor) {
const {deepgramOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.api_key) &&
{DEEPGRAM_API_KEY: sttCredentials.api_key},
...(deepgramOptions.tier) &&
{DEEPGRAM_SPEECH_TIER: deepgramOptions.tier},
...(deepgramOptions.model) &&
{DEEPGRAM_SPEECH_MODEL: deepgramOptions.model},
...(deepgramOptions.punctuate) &&
{DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1},
...(deepgramOptions.profanityFilter) &&
{DEEPGRAM_SPEECH_PROFANITY_FILTER: 1},
...(deepgramOptions.redact) &&
{DEEPGRAM_SPEECH_REDACT: 1},
...(deepgramOptions.diarize) &&
{DEEPGRAM_SPEECH_DIARIZE: 1},
...(deepgramOptions.diarizeVersion) &&
{DEEPGRAM_SPEECH_DIARIZE_VERSION: deepgramOptions.diarizeVersion},
...(deepgramOptions.ner) &&
{DEEPGRAM_SPEECH_NER: 1},
...(deepgramOptions.alternatives) &&
{DEEPGRAM_SPEECH_ALTERNATIVES: deepgramOptions.alternatives},
...(deepgramOptions.numerals) &&
{DEEPGRAM_SPEECH_NUMERALS: deepgramOptions.numerals},
...(deepgramOptions.search) &&
{DEEPGRAM_SPEECH_SEARCH: deepgramOptions.search.join(',')},
...(deepgramOptions.replace) &&
{DEEPGRAM_SPEECH_REPLACE: deepgramOptions.replace.join(',')},
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.trim()).join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.phrase).join(',')}),
...(deepgramOptions.keywords) &&
{DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')},
...('endpointing' in deepgramOptions) &&
{DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing},
...(deepgramOptions.vadTurnoff) &&
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
...(deepgramOptions.tag) &&
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
};
}
else if ('soniox' === vendor) {
const {sonioxOptions = {}} = rOpts;
const {storage = {}} = sonioxOptions;
opts = {
...opts,
...(sttCredentials.api_key) &&
{SONIOX_API_KEY: sttCredentials.api_key},
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{SONIOX_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{SONIOX_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' &&
{SONIOX_HINTS_BOOST: rOpts.hintsBoost}),
...(sonioxOptions.model) &&
{SONIOX_MODEL: sonioxOptions.model},
...((sonioxOptions.profanityFilter || rOpts.profanityFilter) && {SONIOX_PROFANITY_FILTER: 1}),
...(storage?.id && {SONIOX_STORAGE_ID: storage.id}),
...(storage?.id && storage?.title && {SONIOX_STORAGE_TITLE: storage.title}),
...(storage?.id && storage?.disableStoreAudio && {SONIOX_STORAGE_DISABLE_AUDIO: 1}),
...(storage?.id && storage?.disableStoreTranscript && {SONIOX_STORAGE_DISABLE_TRANSCRIPT: 1}),
...(storage?.id && storage?.disableSearch && {SONIOX_STORAGE_DISABLE_SEARCH: 1})
};
}
else if ('ibm' === vendor) {
const {ibmOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.access_token) &&
{IBM_ACCESS_TOKEN: sttCredentials.access_token},
...(sttCredentials.stt_region) &&
{IBM_SPEECH_REGION: sttCredentials.stt_region},
...(sttCredentials.instance_id) &&
{IBM_SPEECH_INSTANCE_ID: sttCredentials.instance_id},
...(ibmOptions.model) &&
{IBM_SPEECH_MODEL: ibmOptions.model},
...(ibmOptions.language_customization_id) &&
{IBM_SPEECH_LANGUAGE_CUSTOMIZATION_ID: ibmOptions.language_customization_id},
...(ibmOptions.acoustic_customization_id) &&
{IBM_SPEECH_ACOUSTIC_CUSTOMIZATION_ID: ibmOptions.acoustic_customization_id},
...(ibmOptions.baseModelVersion) &&
{IBM_SPEECH_BASE_MODEL_VERSION: ibmOptions.baseModelVersion},
...(ibmOptions.watsonMetadata) &&
{IBM_SPEECH_WATSON_METADATA: ibmOptions.watsonMetadata},
...(ibmOptions.watsonLearningOptOut) &&
{IBM_SPEECH_WATSON_LEARNING_OPT_OUT: ibmOptions.watsonLearningOptOut}
};
}
else if ('nvidia' === vendor) {
const {nvidiaOptions = {}} = rOpts;
opts = {
...opts,
...((nvidiaOptions.profanityFilter || rOpts.profanityFilter) && {NVIDIA_PROFANITY_FILTER: 1}),
...(!(nvidiaOptions.profanityFilter || rOpts.profanityFilter) && {NVIDIA_PROFANITY_FILTER: 0}),
...((nvidiaOptions.punctuation || rOpts.punctuation) && {NVIDIA_PUNCTUATION: 1}),
...(!(nvidiaOptions.punctuation || rOpts.punctuation) && {NVIDIA_PUNCTUATION: 0}),
...((rOpts.words || nvidiaOptions.wordTimeOffsets) && {NVIDIA_WORD_TIME_OFFSETS: 1}),
...(!(rOpts.words || nvidiaOptions.wordTimeOffsets) && {NVIDIA_WORD_TIME_OFFSETS: 0}),
...(nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: nvidiaOptions.maxAlternatives}),
...(!nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: 1}),
...(rOpts.model && {NVIDIA_MODEL: rOpts.model}),
...(nvidiaOptions.rivaUri && {NVIDIA_RIVA_URI: nvidiaOptions.rivaUri}),
...(nvidiaOptions.verbatimTranscripts && {NVIDIA_VERBATIM_TRANSCRIPTS: 1}),
...(rOpts.diarization && {NVIDIA_SPEAKER_DIARIZATION: 1}),
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
{NVIDIA_DIARIZATION_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
...(rOpts.separateRecognitionPerChannel && {NVIDIA_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{NVIDIA_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{NVIDIA_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' &&
{NVIDIA_HINTS_BOOST: rOpts.hintsBoost}),
...(nvidiaOptions.customConfiguration &&
{NVIDIA_CUSTOM_CONFIGURATION: JSON.stringify(nvidiaOptions.customConfiguration)}),
};
}
else if (vendor.startsWith('custom:')) {
let {options = {}} = rOpts;
const {auth_token, custom_stt_url} = sttCredentials;
options = {
...options,
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{hints: rOpts.hints}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{hints: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost})
};
opts = {
...opts,
JAMBONZ_STT_API_KEY: auth_token,
JAMBONZ_STT_URL: custom_stt_url,
...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}),
};
}
(stickyVars[vendor] || []).forEach((key) => {
if (!opts[key]) opts[key] = '';
});
return opts;
};
const removeSpeechListeners = (ep) => {
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);
ep.removeCustomEventListener(NuanceTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NuanceTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Transcription);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Connect);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(SonioxTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Transcription);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Connect);
ep.removeCustomEventListener(JambonzTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Error);
};
const setSpeechCredentialsAtRuntime = (recognizer) => {
if (!recognizer) return;
if (recognizer.vendor === 'nuance') {
const {clientId, secret, kryptonEndpoint} = recognizer.nuanceOptions || {};
if (clientId && secret) return {client_id: clientId, secret};
if (kryptonEndpoint) return {krypton_endpoint: kryptonEndpoint};
}
else if (recognizer.vendor === 'nvidia') {
const {rivaUri} = recognizer.nvidiaOptions || {};
if (rivaUri) return {riva_uri: rivaUri};
}
else if (recognizer.vendor === 'deepgram') {
const {apiKey} = recognizer.deepgramOptions || {};
if (apiKey) return {api_key: apiKey};
}
else if (recognizer.vendor === 'soniox') {
const {apiKey} = recognizer.sonioxOptions || {};
if (apiKey) return {api_key: apiKey};
}
else if (recognizer.vendor === 'ibm') {
const {ttsApiKey, ttsRegion, sttApiKey, sttRegion, instanceId} = recognizer.ibmOptions || {};
if (ttsApiKey || sttApiKey) return {
tts_api_key: ttsApiKey,
tts_region: ttsRegion,
stt_api_key: sttApiKey,
stt_region: sttRegion,
instance_id: instanceId
};
}
};
return {
normalizeTranscription,
setChannelVarsForStt,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
};
};

View File

@@ -4,6 +4,7 @@ const short = require('short-uuid');
const {HookMsgTypes} = require('./constants.json'); const {HookMsgTypes} = require('./constants.json');
const Websocket = require('ws'); const Websocket = require('ws');
const snakeCaseKeys = require('./snakecase-keys'); const snakeCaseKeys = require('./snakecase-keys');
const HttpRequestor = require('./http-requestor');
const MAX_RECONNECTS = 5; const MAX_RECONNECTS = 5;
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000; const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
@@ -44,7 +45,7 @@ class WsRequestor extends BaseRequestor {
return; return;
} }
if (this.closedGracefully) { if (this.closedGracefully) {
this.logger.debug(`WsRequestor:request - discarding ${type} because socket was closed gracefully`); this.logger.debug(`WsRequestor:request - discarding ${type} because we closed the socket`);
return; return;
} }
@@ -52,14 +53,8 @@ class WsRequestor extends BaseRequestor {
/* if we have an absolute url, and it is http then do a standard webhook */ /* if we have an absolute url, and it is http then do a standard webhook */
if (this._isAbsoluteUrl(url) && url.startsWith('http')) { if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
const HttpRequestor = require('./http-requestor');
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)'); this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
const h = typeof hook === 'object' ? hook : {url: hook}; const requestor = new HttpRequestor(this.logger, this.account_sid, hook, this.secret);
const requestor = new HttpRequestor(this.logger, this.account_sid, h, this.secret);
if (type === 'session:redirect') {
this.close();
this.emit('handover', requestor);
}
return requestor.request(type, hook, params, httpHeaders); return requestor.request(type, hook, params, httpHeaders);
} }
@@ -74,7 +69,7 @@ class WsRequestor extends BaseRequestor {
this.connectInProgress = true; this.connectInProgress = true;
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection`); this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection`);
if (this.connections >= MAX_RECONNECTS) { if (this.connections >= MAX_RECONNECTS) {
return Promise.reject(`max attempts connecting to ${this.url}`); throw new Error(`max attempts connecting to ${this.url}`);
} }
try { try {
const startAt = process.hrtime(); const startAt = process.hrtime();
@@ -84,7 +79,7 @@ class WsRequestor extends BaseRequestor {
} catch (err) { } catch (err) {
this.logger.info({url, err}, 'WsRequestor:request - failed connecting'); this.logger.info({url, err}, 'WsRequestor:request - failed connecting');
this.connectInProgress = false; this.connectInProgress = false;
return Promise.reject(err); throw err;
} }
} }
assert(this.ws); assert(this.ws);
@@ -96,9 +91,6 @@ class WsRequestor extends BaseRequestor {
assert.ok(url, 'WsRequestor:request url was not provided'); assert.ok(url, 'WsRequestor:request url was not provided');
const msgid = short.generate(); const msgid = short.generate();
// save initial msgid in case we need to reconnect during initial session:new
if (type === 'session:new') this._initMsgId = msgid;
const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {}; const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {};
const obj = { const obj = {
type, type,
@@ -121,18 +113,8 @@ class WsRequestor extends BaseRequestor {
//this.logger.debug({obj}, `websocket: sending (${url})`); //this.logger.debug({obj}, `websocket: sending (${url})`);
/* special case: reconnecting before we received ack to session:new */
let reconnectingWithoutAck = false;
if (type === 'session:reconnect' && this._initMsgId) {
reconnectingWithoutAck = true;
const obj = this.messagesInFlight.get(this._initMsgId);
this.messagesInFlight.delete(this._initMsgId);
this.messagesInFlight.set(msgid, obj);
this._initMsgId = msgid;
}
/* simple notifications */ /* simple notifications */
if (['call:status', 'verb:status', 'jambonz:error'].includes(type) || reconnectingWithoutAck) { if (['call:status', 'jambonz:error', 'session:reconnect'].includes(type)) {
this.ws.send(JSON.stringify(obj), () => { this.ws.send(JSON.stringify(obj), () => {
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`); this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
sendQueuedMsgs(); sendQueuedMsgs();
@@ -144,7 +126,7 @@ class WsRequestor extends BaseRequestor {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
/* give the far end a reasonable amount of time to ack our message */ /* give the far end a reasonable amount of time to ack our message */
const timer = setTimeout(() => { const timer = setTimeout(() => {
const {failure} = this.messagesInFlight.get(msgid) || {}; const {failure} = this.messagesInFlight.get(msgid);
failure && failure(`timeout from far end for msgid ${msgid}`); failure && failure(`timeout from far end for msgid ${msgid}`);
this.messagesInFlight.delete(msgid); this.messagesInFlight.delete(msgid);
}, RESPONSE_TIMEOUT_MS); }, RESPONSE_TIMEOUT_MS);
@@ -156,7 +138,7 @@ class WsRequestor extends BaseRequestor {
success: (response) => { success: (response) => {
clearTimeout(timer); clearTimeout(timer);
const rtt = this._roundTrip(startAt); const rtt = this._roundTrip(startAt);
this.logger.debug({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`); this.logger.info({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']); this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']);
resolve(response); resolve(response);
}, },
@@ -176,14 +158,20 @@ class WsRequestor extends BaseRequestor {
close() { close() {
this.closedGracefully = true; this.closedGracefully = true;
this.logger.debug('WsRequestor:close closing socket'); this.logger.info('WsRequestor:close closing socket');
try { try {
if (this.ws) { if (this.ws) {
this.ws.close(1000); this.ws.close();
this.ws.removeAllListeners(); this.ws.removeAllListeners();
this.ws = null;
} }
this._clearPendingMessages();
for (const [msgid, obj] of this.messagesInFlight) {
const {timer} = obj;
clearTimeout(timer);
obj.failure(`abandoning msgid ${msgid} since we have closed the socket`);
}
this.messagesInFlight.clear();
} catch (err) { } catch (err) {
this.logger.info({err}, 'WsRequestor: Error closing socket'); this.logger.info({err}, 'WsRequestor: Error closing socket');
} }
@@ -199,7 +187,7 @@ class WsRequestor extends BaseRequestor {
followRedirects: true, followRedirects: true,
maxRedirects: 2, maxRedirects: 2,
handshakeTimeout, handshakeTimeout,
maxPayload: process.env.JAMBONES_WS_MAX_PAYLOAD ? parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024, maxPayload: 8096,
}; };
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`}; if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
@@ -219,6 +207,7 @@ class WsRequestor extends BaseRequestor {
} }
_setHandlers(ws) { _setHandlers(ws) {
this.logger.debug('WsRequestor:_setHandlers');
ws ws
.once('open', this._onOpen.bind(this, ws)) .once('open', this._onOpen.bind(this, ws))
.once('close', this._onClose.bind(this)) .once('close', this._onClose.bind(this))
@@ -227,15 +216,6 @@ class WsRequestor extends BaseRequestor {
.on('error', this._onError.bind(this)); .on('error', this._onError.bind(this));
} }
_clearPendingMessages() {
for (const [msgid, obj] of this.messagesInFlight) {
const {timer} = obj;
clearTimeout(timer);
if (!this._initMsgId) obj.failure(`abandoning msgid ${msgid} since socket is closed`);
}
this.messagesInFlight.clear();
}
_onError(err) { _onError(err) {
if (this.connections > 0) { if (this.connections > 0) {
this.logger.info({url: this.url, err}, 'WsRequestor:_onError'); this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
@@ -273,14 +253,12 @@ class WsRequestor extends BaseRequestor {
}, 'WsRequestor - unexpected response'); }, 'WsRequestor - unexpected response');
this.emit('connection-failure'); this.emit('connection-failure');
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`)); this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
this.connections++;
} }
_onSocketClosed() { _onSocketClosed() {
this.ws = null; this.ws = null;
this.emit('connection-dropped'); this.emit('connection-dropped');
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) { if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
if (!this._initMsgId) this._clearPendingMessages();
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`); this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
setTimeout(() => { setTimeout(() => {
this.logger.debug( this.logger.debug(
@@ -308,7 +286,7 @@ class WsRequestor extends BaseRequestor {
const obj = JSON.parse(content); const obj = JSON.parse(content);
const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj; const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
//this.logger.debug({obj}, 'WsRequestor:request websocket: received'); this.logger.debug({obj}, 'WsRequestor:request websocket: received');
assert.ok(type, 'type property not supplied'); assert.ok(type, 'type property not supplied');
switch (type) { switch (type) {
@@ -332,13 +310,12 @@ class WsRequestor extends BaseRequestor {
} }
_recvAck(msgid, data) { _recvAck(msgid, data) {
this._initMsgId = null;
const obj = this.messagesInFlight.get(msgid); const obj = this.messagesInFlight.get(msgid);
if (!obj) { if (!obj) {
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`); this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
return; return;
} }
//this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`); this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
this.messagesInFlight.delete(msgid); this.messagesInFlight.delete(msgid);
const {success} = obj; const {success} = obj;
success && success(data); success && success(data);
@@ -346,7 +323,7 @@ class WsRequestor extends BaseRequestor {
_recvCommand(msgid, command, call_sid, queueCommand, data) { _recvCommand(msgid, command, call_sid, queueCommand, data) {
// TODO: validate command // TODO: validate command
//this.logger.debug({msgid, command, call_sid, queueCommand, data}, 'received command'); this.logger.info({msgid, command, call_sid, queueCommand, data}, 'received command');
this.emit('command', {msgid, command, call_sid, queueCommand, data}); this.emit('command', {msgid, command, call_sid, queueCommand, data});
} }
} }

12083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "v0.8.1", "version": "v0.7.5",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 10.16.0" "node": ">= 10.16.0"
@@ -16,59 +16,62 @@
"type": "git", "type": "git",
"url": "https://github.com/jambonz/jambonz-feature-server.git" "url": "https://github.com/jambonz/jambonz-feature-server.git"
}, },
"bugs": {}, "bugs": {
"url": "https://github.com/jambonz/jambonz-feature-server/issues"
},
"scripts": { "scripts": {
"start": "node app", "start": "node app",
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=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:JambonzR0ck$: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=info 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", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib" "jslint": "eslint app.js lib"
}, },
"dependencies": { "dependencies": {
"@jambonz/db-helpers": "^0.7.4", "@cognigy/socket-client": "^4.5.5",
"@jambonz/db-helpers": "^0.6.16",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.7.0", "@jambonz/mw-registrar": "^0.2.1",
"@jambonz/speech-utils": "^0.0.6", "@jambonz/realtimedb-helpers": "^0.4.27",
"@jambonz/stats-collector": "^0.1.6", "@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.2.5", "@jambonz/time-series": "^0.1.6",
"@jambonz/verb-specifications": "^0.0.11", "@opentelemetry/api": "^1.1.0",
"@opentelemetry/api": "^1.4.0", "@opentelemetry/exporter-jaeger": "^1.1.0",
"@opentelemetry/exporter-jaeger": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0", "@opentelemetry/exporter-zipkin": "^1.1.0",
"@opentelemetry/exporter-zipkin": "^1.9.0", "@opentelemetry/instrumentation": "^0.27.0",
"@opentelemetry/instrumentation": "^0.35.0", "@opentelemetry/instrumentation-express": "^0.28.0",
"@opentelemetry/resources": "^1.9.0", "@opentelemetry/instrumentation-http": "^0.27.0",
"@opentelemetry/sdk-trace-base": "^1.9.0", "@opentelemetry/instrumentation-pino": "^0.28.1",
"@opentelemetry/sdk-trace-node": "^1.9.0", "@opentelemetry/resources": "^1.1.0",
"@opentelemetry/semantic-conventions": "^1.9.0", "@opentelemetry/sdk-trace-base": "^1.1.0",
"aws-sdk": "^2.1313.0", "@opentelemetry/sdk-trace-node": "^1.1.0",
"@opentelemetry/semantic-conventions": "^1.1.0",
"aws-sdk": "^2.1073.0",
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "cidr-matcher": "^2.1.1",
"debug": "^4.3.2",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.20", "drachtio-fsmrf": "^2.0.13",
"drachtio-srf": "^4.5.23", "drachtio-srf": "^4.4.61",
"express": "^4.18.2", "express": "^4.17.1",
"ip": "^1.1.8", "helmet": "^5.0.2",
"moment": "^2.29.4", "ip": "^1.1.5",
"parse-url": "^8.1.0", "moment": "^2.29.2",
"pino": "^8.8.0", "parse-url": "^5.0.7",
"polly-ssml-split": "^0.1.0", "pino": "^6.13.4",
"proxyquire": "^2.1.3", "short-uuid": "^4.2.0",
"sdp-transform": "^2.14.1",
"short-uuid": "^4.2.2",
"sinon": "^15.0.1",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^5.19.1", "uuid": "^8.3.2",
"uuid-random": "^1.3.2", "verify-aws-sns-signature": "^0.0.6",
"verify-aws-sns-signature": "^0.1.0", "ws": "^8.5.0",
"ws": "^8.9.0",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
"devDependencies": { "devDependencies": {
"clear-module": "^4.1.2", "async": "^3.2.0",
"eslint": "^7.32.0", "clear-module": "^4.1.1",
"eslint": "^7.20.0",
"eslint-plugin-promise": "^4.3.1", "eslint-plugin-promise": "^4.3.1",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"tape": "^5.6.1" "tape": "^5.2.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"bufferutil": "^4.0.6", "bufferutil": "^4.0.6",

View File

@@ -1,102 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'config: listen\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const from = "config_listen_success";
let verbs = [
{
"verb": "config",
"listen": {
"enable": true,
"url": `ws://172.38.0.60:3000/${from}`
}
},
{
"verb": "pause",
"length": 5
}
];
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
t.pass('config: successfully started background listen');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'config: listen - stop\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const from = "config_listen_success";
let verbs = [
{
"verb": "config",
"listen": {
"enable": true,
"url": `ws://172.38.0.60:3000/${from}`
}
},
{
"verb": "pause",
"length": 1
},
{
"verb": "config",
"listen": {
"enable": false
}
},
{
"verb": "pause",
"length": 3
}
];
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
t.pass('config: successfully started then stopped background listen');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,108 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
const getJSON = bent('json')
const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('test create-call timeout', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// give UAS app time to come up
const p = sippUac('uas-timeout-cancel.xml', '172.38.0.10');
await waitFor(1000);
// GIVEN
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
'timeout': 1,
"call_hook": {
"url": "https://public-apps.jambonz.us/hello-world",
"method": "POST"
},
"from": "15083718299",
"to": {
"type": "phone",
"number": "15583084809"
}});
//THEN
await p;
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('test create-call call-hook basic authentication', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let from = 'call_hook_basic_authentication';
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
// Give UAS app time to come up
const p = sippUac('uas.xml', '172.38.0.10', from);
await waitFor(1000);
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
"call_hook": {
"url": "http://127.0.0.1:3100/",
"method": "POST",
"username": "username",
"password": "password"
},
"from": from,
"to": {
"type": "phone",
"number": "15583084809"
}});
let verbs = [
{
"verb": "say",
"text": "hello"
}
];
provisionCallHook(from, verbs);
//THEN
await p;
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}`)
t.ok(obj.headers.Authorization = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
'create-call: call-hook contains basic authentication header');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -22,17 +22,11 @@ test('creating schema', (t) => {
const google_credential = encrypt(process.env.GCP_JSON_KEY); const google_credential = encrypt(process.env.GCP_JSON_KEY);
const aws_credential = encrypt(JSON.stringify({ const aws_credential = encrypt(JSON.stringify({
access_key_id: process.env.AWS_ACCESS_KEY_ID, access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY, secret_access_key: process.env.AWS_SECRET_ACCESS_KEY
aws_region: process.env.AWS_REGION
}));
const microsoft_credential = encrypt(JSON.stringify({
region: process.env.MICROSOFT_REGION || 'useast',
api_key: process.env.MICROSOFT_API_KEY || '1234567890'
})); }));
const cmd = ` const cmd = `
UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google'; UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';
UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws'; UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';
UPDATE speech_credentials SET credential='${microsoft_credential}' WHERE vendor='microsoft';
`; `;
const path = `${__dirname}/.creds.sql`; const path = `${__dirname}/.creds.sql`;
fs.writeFileSync(path, cmd); fs.writeFileSync(path, cmd);

View File

@@ -1,9 +0,0 @@
{
"say": {
"text": "<speak>I already told you <emphasis level=\"strong\">I already told you I already told you I already told you I already told you! I already told you I already told you I already told you I already told you? I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you told I already told you I already told you told I already told you I already told you. I already told you <break time=\"3s\"/> I really like that person!</emphasis> this is another long text.</speak>",
"synthesizer": {
"vendor": "google",
"language": "en-US"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"say": {
"text": "<speak>I already told you I already told you I already told you I already told you I already told you! I already told you I already told you I already told you I already told you? I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you told I already told you I already told you told I already told you I already told you. I already told you <break time=\"3s\"/> I <emphasis level=\"strong\">really like that person!</emphasis> this is another long text.</speak>",
"synthesizer": {
"vendor": "google",
"language": "en-US"
}
}
}

View File

@@ -217,7 +217,6 @@ CREATE TABLE `applications` (
`call_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for inbound calls ', `call_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for inbound calls ',
`call_status_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for call status events', `call_status_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for call status events',
`messaging_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for inbound SMS/MMS ', `messaging_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for inbound SMS/MMS ',
`app_json` VARCHAR(16384),
`speech_synthesis_vendor` varchar(64) NOT NULL DEFAULT 'google', `speech_synthesis_vendor` varchar(64) NOT NULL DEFAULT 'google',
`speech_synthesis_language` varchar(12) NOT NULL DEFAULT 'en-US', `speech_synthesis_language` varchar(12) NOT NULL DEFAULT 'en-US',
`speech_synthesis_voice` varchar(64) DEFAULT NULL, `speech_synthesis_voice` varchar(64) DEFAULT NULL,
@@ -246,14 +245,12 @@ CREATE TABLE `applications` (
LOCK TABLES `applications` WRITE; LOCK TABLES `applications` WRITE;
/*!40000 ALTER TABLE `applications` DISABLE KEYS */; /*!40000 ALTER TABLE `applications` DISABLE KEYS */;
INSERT INTO `applications` VALUES ('0dddaabf-0a30-43e3-84e8-426873b1a78b','decline call',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US'); INSERT INTO `applications` VALUES ('0dddaabf-0a30-43e3-84e8-426873b1a78b','decline call',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('308b4f41-1a18-4052-b89a-c054e75ce242','say',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US'); INSERT INTO `applications` VALUES ('308b4f41-1a18-4052-b89a-c054e75ce242','say',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('24d0f6af-e976-44dd-a2e8-41c7b55abe33','say account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US'); INSERT INTO `applications` VALUES ('24d0f6af-e976-44dd-a2e8-41c7b55abe33','say account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('17461c69-56b5-4dab-ad83-1c43a0f93a3d','gather',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','10692465-a511-4277-9807-b7157e4f81e1','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US'); INSERT INTO `applications` VALUES ('17461c69-56b5-4dab-ad83-1c43a0f93a3d','gather',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','10692465-a511-4277-9807-b7157e4f81e1','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('baf9213b-5556-4c20-870c-586392ed246f','transcribe',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US'); INSERT INTO `applications` VALUES ('baf9213b-5556-4c20-870c-586392ed246f','transcribe',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('ae026ab5-3029-47b4-9d7c-236e3a4b4ebe','transcribe account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US'); INSERT INTO `applications` VALUES ('ae026ab5-3029-47b4-9d7c-236e3a4b4ebe','transcribe account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('195d9507-6a42-46a8-825f-f009e729d023','sip info',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c9113e7a-741f-48b9-96c1-f2f78176eeb3','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('0dddaabf-0a30-43e3-84e8-426873b1a78c','app json',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'[{"verb": "play","url": "silence_stream://5000"}]','google','en-US','en-US-Standard-C','google','en-US');
/*!40000 ALTER TABLE `applications` ENABLE KEYS */; /*!40000 ALTER TABLE `applications` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -452,8 +449,6 @@ INSERT INTO `phone_numbers` VALUES ('e686a320-0725-418f-be65-532159bdc3ed','1617
INSERT INTO `phone_numbers` VALUES ('05eeed62-b29b-4679-bf38-d7a4e318be44','16174000003','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','17461c69-56b5-4dab-ad83-1c43a0f93a3d', NULL); INSERT INTO `phone_numbers` VALUES ('05eeed62-b29b-4679-bf38-d7a4e318be44','16174000003','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','17461c69-56b5-4dab-ad83-1c43a0f93a3d', NULL);
INSERT INTO `phone_numbers` VALUES ('f3c53863-b629-4cf6-9dcb-c7fb7072314b','16174000004','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','baf9213b-5556-4c20-870c-586392ed246f', NULL); INSERT INTO `phone_numbers` VALUES ('f3c53863-b629-4cf6-9dcb-c7fb7072314b','16174000004','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','baf9213b-5556-4c20-870c-586392ed246f', NULL);
INSERT INTO `phone_numbers` VALUES ('f6416c17-829a-4f11-9c32-f0d00e4a9ae9','16174000005','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','ae026ab5-3029-47b4-9d7c-236e3a4b4ebe', NULL); INSERT INTO `phone_numbers` VALUES ('f6416c17-829a-4f11-9c32-f0d00e4a9ae9','16174000005','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','ae026ab5-3029-47b4-9d7c-236e3a4b4ebe', NULL);
INSERT INTO `phone_numbers` VALUES ('964d0581-9627-44cb-be20-8118050406b2','16174000006','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','195d9507-6a42-46a8-825f-f009e729d023', NULL);
INSERT INTO `phone_numbers` VALUES ('964d0581-9627-44cb-be20-8118050406b3','16174000007','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a78c', NULL);
/*!40000 ALTER TABLE `phone_numbers` ENABLE KEYS */; /*!40000 ALTER TABLE `phone_numbers` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -617,10 +612,7 @@ CREATE TABLE `speech_credentials` (
LOCK TABLES `speech_credentials` WRITE; LOCK TABLES `speech_credentials` WRITE;
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */; /*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
INSERT INTO `speech_credentials` VALUES 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);
('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),
('2add347f-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','microsoft','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',1,1,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */; /*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -744,7 +736,6 @@ INSERT INTO `webhooks` VALUES ('c71e79db-24f2-4866-a3ee-febb0f97b341','http://12
INSERT INTO `webhooks` VALUES ('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL); INSERT INTO `webhooks` VALUES ('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL); INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL); INSERT INTO `webhooks` VALUES ('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('c9113e7a-741f-48b9-96c1-f2f78176eeb3','http://127.0.0.1:3104/','POST',NULL,NULL);
/*!40000 ALTER TABLE `webhooks` ENABLE KEYS */; /*!40000 ALTER TABLE `webhooks` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

View File

@@ -4,8 +4,6 @@ SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips; DROP TABLE IF EXISTS account_static_ips;
DROP TABLE IF EXISTS account_limits;
DROP TABLE IF EXISTS account_products; DROP TABLE IF EXISTS account_products;
DROP TABLE IF EXISTS account_subscriptions; DROP TABLE IF EXISTS account_subscriptions;
@@ -20,12 +18,6 @@ DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes; DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS password_settings;
DROP TABLE IF EXISTS user_permissions;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS predefined_sip_gateways; DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways; DROP TABLE IF EXISTS predefined_smpp_gateways;
@@ -44,8 +36,6 @@ DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS ms_teams_tenants; DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS service_provider_limits;
DROP TABLE IF EXISTS signup_history; DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses; DROP TABLE IF EXISTS smpp_addresses;
@@ -79,15 +69,6 @@ private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
PRIMARY KEY (account_static_ip_sid) PRIMARY KEY (account_static_ip_sid)
); );
CREATE TABLE account_limits
(
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
);
CREATE TABLE account_subscriptions CREATE TABLE account_subscriptions
( (
account_subscription_sid CHAR(36) NOT NULL UNIQUE , account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
@@ -142,21 +123,6 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f
PRIMARY KEY (lcr_route_sid) PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table'; ) COMMENT='Least cost routing table';
CREATE TABLE password_settings
(
min_password_length INTEGER NOT NULL DEFAULT 8,
require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
);
CREATE TABLE predefined_carriers CREATE TABLE predefined_carriers
( (
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE , predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
@@ -262,15 +228,6 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid) PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant'; ) COMMENT='A Microsoft Teams customer tenant';
CREATE TABLE service_provider_limits
(
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
);
CREATE TABLE signup_history CREATE TABLE signup_history
( (
email VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL,
@@ -326,7 +283,6 @@ email_activation_code VARCHAR(16),
email_validated BOOLEAN NOT NULL DEFAULT false, email_validated BOOLEAN NOT NULL DEFAULT false,
phone_validated BOOLEAN NOT NULL DEFAULT false, phone_validated BOOLEAN NOT NULL DEFAULT false,
email_content_opt_out BOOLEAN NOT NULL DEFAULT false, email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (user_sid) PRIMARY KEY (user_sid)
); );
@@ -354,20 +310,9 @@ smpp_password VARCHAR(64),
smpp_enquire_link_interval INTEGER DEFAULT 0, smpp_enquire_link_interval INTEGER DEFAULT 0,
smpp_inbound_system_id VARCHAR(255), smpp_inbound_system_id VARCHAR(255),
smpp_inbound_password VARCHAR(64), smpp_inbound_password VARCHAR(64),
register_from_user VARCHAR(128),
register_from_domain VARCHAR(255),
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (voip_carrier_sid) PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls'; ) COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
);
CREATE TABLE smpp_gateways CREATE TABLE smpp_gateways
( (
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE , smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
@@ -385,7 +330,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers CREATE TABLE phone_numbers
( (
phone_number_sid CHAR(36) UNIQUE , phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL UNIQUE , number VARCHAR(32) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36), voip_carrier_sid CHAR(36),
account_sid CHAR(36), account_sid CHAR(36),
application_sid CHAR(36), application_sid CHAR(36),
@@ -435,7 +380,6 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ', call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events', call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ', messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json VARCHAR(16384),
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google', speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US', speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64), speech_synthesis_voice VARCHAR(64),
@@ -474,11 +418,6 @@ disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
trial_end_date DATETIME, trial_end_date DATETIME,
deactivated_reason VARCHAR(255), deactivated_reason VARCHAR(255),
device_to_call_ratio INTEGER NOT NULL DEFAULT 5, device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
subspace_client_id VARCHAR(255),
subspace_client_secret VARCHAR(255),
subspace_sip_teleport_id VARCHAR(255),
subspace_sip_teleport_destinations VARCHAR(255),
siprec_hook_sid CHAR(36),
PRIMARY KEY (account_sid) PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services'; ) COMMENT='An enterprise that uses the platform for comm services';
@@ -486,23 +425,19 @@ CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_
CREATE INDEX account_sid_idx ON account_static_ips (account_sid); CREATE INDEX account_sid_idx ON account_static_ips (account_sid);
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid); ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX account_sid_idx ON account_limits (account_sid);
ALTER TABLE account_limits ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid) ON DELETE CASCADE;
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid); CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid); CREATE INDEX account_sid_idx ON account_subscriptions (account_sid);
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code); CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid); CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid); ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid); CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid); CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid); CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid); CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
@@ -521,14 +456,14 @@ ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REF
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid); CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
CREATE INDEX account_sid_idx ON account_offers (account_sid); CREATE INDEX account_sid_idx ON account_offers (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX product_sid_idx ON account_offers (product_sid); CREATE INDEX product_sid_idx ON account_offers (product_sid);
ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid); ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid);
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid); CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
CREATE INDEX account_sid_idx ON api_keys (account_sid); CREATE INDEX account_sid_idx ON api_keys (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid); CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -542,53 +477,44 @@ ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid); CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn); CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
CREATE INDEX service_provider_sid_idx ON service_provider_limits (service_provider_sid);
ALTER TABLE service_provider_limits ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid) ON DELETE CASCADE;
CREATE INDEX email_idx ON signup_history (email); CREATE INDEX email_idx ON signup_history (email);
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid); CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid); CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid); CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid); CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid); CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON speech_credentials (account_sid); CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX user_sid_idx ON users (user_sid); CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email); CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone); CREATE INDEX phone_idx ON users (phone);
CREATE INDEX account_sid_idx ON users (account_sid); CREATE INDEX account_sid_idx ON users (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid); CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX email_activation_code_idx ON users (email_activation_code); CREATE INDEX email_activation_code_idx ON users (email_activation_code);
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
CREATE INDEX account_sid_idx ON voip_carriers (account_sid); CREATE INDEX account_sid_idx ON voip_carriers (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid); CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid); CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
@@ -598,12 +524,12 @@ CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid); CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port); CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
@@ -619,10 +545,10 @@ CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
CREATE INDEX application_sid_idx ON applications (application_sid); CREATE INDEX application_sid_idx ON applications (application_sid);
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid); CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON applications (account_sid); CREATE INDEX account_sid_idx ON applications (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_12 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid); ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -638,7 +564,7 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
CREATE INDEX account_sid_idx ON accounts (account_sid); CREATE INDEX account_sid_idx ON accounts (account_sid);
CREATE INDEX sip_realm_idx ON accounts (sip_realm); CREATE INDEX sip_realm_idx ON accounts (sip_realm);
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid); CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_10 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid); ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -646,6 +572,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid); ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid); SET FOREIGN_KEY_CHECKS=0;
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -1,212 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'dial-phone\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// wait for fs connected to drachtio server.
await new Promise(r => setTimeout(r, 1000));
// GIVEN
const from = "dial_success";
let verbs = [
{
"verb": "dial",
"callerId": from,
"actionHook": "/actionHook",
"timeLimit": 5,
"target": [
{
"type": "phone",
"number": "15083084809"
}
]
}
];
provisionCallHook(from, verbs);
// THEN
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
"call_hook": {
"url": "http://127.0.0.1:3100/",
"method": "POST",
},
"from": from,
"to": {
"type": "phone",
"number": "15583084808"
}});
await p;
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.from === from,
'dial: succeeds actionHook');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'dial-sip\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// wait for fs connected to drachtio server.
await new Promise(r => setTimeout(r, 1000));
// GIVEN
const from = "dial_sip";
let verbs = [
{
"verb": "dial",
"callerId": from,
"actionHook": "/actionHook",
"dtmfCapture":["*2", "*3"],
"target": [
{
"type": "sip",
"sipUri": "sip:15083084809@jambonz.com"
}
]
}
];
provisionCallHook(from, verbs);
// THEN
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
"call_hook": {
"url": "http://127.0.0.1:3100/",
"method": "POST",
},
"from": from,
"to": {
"type": "phone",
"number": "15583084808"
}});
await new Promise(r => setTimeout(r, 2000));
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}`);
const callSid = obj.body.call_sid;
post = bent('http://127.0.0.1:3000/', 'POST', 202);
await post(`v1/updateCall/${callSid}`, {
"call_status": "completed"
});
await p;
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.from === from,
'dial: succeeds actionHook');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'dial-user\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// wait for fs connected to drachtio server.
await new Promise(r => setTimeout(r, 1000));
// GIVEN
const from = "dial_user";
let verbs = [
{
"verb": "dial",
"callerId": from,
"actionHook": "/actionHook",
"target": [
{
"type": "user",
"name": "user110@jambonz.com"
}
]
}
];
provisionCallHook(from, verbs);
// THEN
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
"call_hook": {
"url": "http://127.0.0.1:3100/",
"method": "POST",
},
"from": from,
"to": {
"type": "phone",
"number": "15583084808"
}});
await new Promise(r => setTimeout(r, 2000));
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}`);
const callSid = obj.body.call_sid;
post = bent('http://127.0.0.1:3000/', 'POST', 202);
await post(`v1/updateCall/${callSid}`, {
"call_status": "completed"
});
await p;
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.from === from,
'dial: succeeds actionHook');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -12,7 +12,7 @@ services:
platform: linux/x86_64 platform: linux/x86_64
ports: ports:
- "3360:3306" - "3360:3306"
environment: environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
healthcheck: healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"] test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"]
@@ -44,7 +44,7 @@ services:
drachtio: drachtio:
image: drachtio/drachtio-server:latest image: drachtio/drachtio-server:latest
restart: always restart: always
command: drachtio --contact "sip:*;transport=udp" --mtu 4096 --address 0.0.0.0 --port 9022 command: drachtio --contact "sip:*;transport=udp,tcp" --address 0.0.0.0 --port 9022
ports: ports:
- "9060:9022/tcp" - "9060:9022/tcp"
networks: networks:
@@ -57,7 +57,7 @@ services:
condition: service_healthy condition: service_healthy
freeswitch: freeswitch:
image: drachtio/drachtio-freeswitch-mrf:0.4.18 image: drachtio/drachtio-freeswitch-mrf:v1.10.1-full
restart: always restart: always
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100 command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
environment: environment:
@@ -68,15 +68,17 @@ services:
- /tmp:/tmp - /tmp:/tmp
- ./credentials:/opt/credentials - ./credentials:/opt/credentials
healthcheck: healthcheck:
test: ['CMD', 'fs_cli' ,'-p', 'JambonzR0ck$$', '-x', '"sofia status"'] test: ['CMD', 'fs_cli' ,'-x', '"sofia status"']
timeout: 5s timeout: 5s
retries: 15 retries: 15
networks: networks:
fs: fs:
ipv4_address: 172.38.0.51 ipv4_address: 172.38.0.51
webhook-scaffold: webhook-decline:
image: jambonz/webhook-test-scaffold:latest image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/decline.json
ports: ports:
- "3100:3000/tcp" - "3100:3000/tcp"
volumes: volumes:
@@ -85,6 +87,42 @@ services:
fs: fs:
ipv4_address: 172.38.0.60 ipv4_address: 172.38.0.60
webhook-say:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/say.json
ports:
- "3101:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.61
webhook-gather:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/gather.json
ports:
- "3102:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.62
webhook-transcribe:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/transcribe.json
ports:
- "3103:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.63
influxdb: influxdb:
image: influxdb:1.8 image: influxdb:1.8
ports: ports:

View File

@@ -1,5 +1,6 @@
const test = require('tape') ; const test = require('tape') ;
const exec = require('child_process').exec ; const exec = require('child_process').exec ;
const async = require('async');
test('starting docker network..takes a bit for mysql and freeswitch to come up..patience..', (t) => { test('starting docker network..takes a bit for mysql and freeswitch to come up..patience..', (t) => {
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => { exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {

View File

@@ -3,7 +3,6 @@ const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent'); const bent = require('bent');
const getJSON = bent('json') const getJSON = bent('json')
const clearModule = require('clear-module'); const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -17,238 +16,16 @@ function connect(connectable) {
}); });
} }
test('\'gather\' test - google', async(t) => { test('\'gather\' and \'transcribe\' tests', async(t) => {
if (!process.env.GCP_JSON_KEY) {
t.pass('skipping google tests');
return t.end();
}
clearModule.all(); clearModule.all();
const {srf, disconnect} = require('../app'); const {srf, disconnect} = require('../app');
try { try {
await connect(srf); await connect(srf);
// GIVEN await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10');
let verbs = [ let obj = await getJSON('http://127.0.0.1:3102/actionHook');
{ t.ok(obj.speech.alternatives[0].transcript = 'I\'d like to speak to customer support',
"verb": "gather", 'gather: succeeds when using account credentials');
"input": ["speech"],
"recognizer": {
"vendor": "google",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"timeout": 10,
"actionHook": "/actionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'gather: succeeds when using google credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'gather\' test - default (google)', async(t) => {
if (!process.env.GCP_JSON_KEY) {
t.pass('skipping google tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "gather",
"input": ["speech"],
"timeout": 10,
"actionHook": "/actionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase() === 'i\'d like to speak to customer support',
'gather: succeeds when using default (google) credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'gather\' test - microsoft', async(t) => {
if (!process.env.MICROSOFT_REGION || !process.env.MICROSOFT_API_KEY) {
t.pass('skipping microsoft tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "gather",
"input": ["speech"],
"recognizer": {
"vendor": "microsoft",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"timeout": 10,
"actionHook": "/actionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'gather: succeeds when using microsoft credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'gather\' test - aws', async(t) => {
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
t.pass('skipping aws tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "gather",
"input": ["speech"],
"recognizer": {
"vendor": "aws",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"timeout": 10,
"actionHook": "/actionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'gather: succeeds when using aws credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'gather\' test - deepgram', async(t) => {
if (!process.env.DEEPGRAM_API_KEY ) {
t.pass('skipping deepgram tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "gather",
"input": ["speech"],
"recognizer": {
"vendor": "deepgram",
"hints": ["customer support", "sales", "human resources", "HR"],
"deepgramOptions": {
"apiKey": process.env.DEEPGRAM_API_KEY
}
},
"timeout": 10,
"actionHook": "/actionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'gather: succeeds when using deepgram credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'gather\' test - soniox', async(t) => {
if (!process.env.SONIOX_API_KEY ) {
t.pass('skipping soniox tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "gather",
"input": ["speech"],
"recognizer": {
"vendor": "deepgram",
"hints": ["customer support", "sales", "human resources", "HR"],
"deepgramOptions": {
"apiKey": process.env.SONIOX_API_KEY
}
},
"timeout": 10,
"actionHook": "/actionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'gather: succeeds when using soniox credentials');
disconnect(); disconnect();
} catch (err) { } catch (err) {

View File

@@ -1,18 +1,9 @@
require('./ws-requestor-unit-test');
require('./unit-tests'); require('./unit-tests');
require('./docker_start'); require('./docker_start');
require('./create-test-db'); require('./create-test-db');
require('./account-validation-tests'); require('./account-validation-tests');
require('./dial-tests');
require('./webhooks-tests'); require('./webhooks-tests');
require('./say-tests'); require('./say-tests');
require('./gather-tests'); require('./gather-tests');
require('./transcribe-tests');
require('./sip-request-tests');
require('./create-call-test');
require('./play-tests');
require('./sip-refer-tests');
require('./listen-tests');
require('./config-test');
require('./remove-test-db'); require('./remove-test-db');
require('./docker_stop'); require('./docker_stop');

View File

@@ -1,149 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'listen-success\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const from = "listen_success";
let verbs = [
{
"verb": "listen",
"url": `ws://172.38.0.60:3000/${from}`,
"mixType" : "mono",
"actionHook": "/actionHook",
"playBeep": true,
}
];
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
t.ok(38000 <= obj.count, 'listen: success incoming call audio');
obj = await getJSON(`http://127.0.0.1:3100/ws_metadata/${from}`);
t.ok(obj.metadata.from === from && obj.metadata.sampleRate === 8000, 'listen: success metadata');
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.from === from,
'listen: succeeds actionHook');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'listen-maxLength\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let from = "listen_timeout";
let verbs = [
{
"verb": "listen",
"url": `ws://172.38.0.60:3000/${from}`,
"mixType" : "mixed",
"timeout": 2,
"maxLength": 2
}
];
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
t.ok(30000 <= obj.count, 'listen: success maxLength incoming call audio');
obj = await getJSON(`http://127.0.0.1:3100/ws_metadata/${from}`);
t.ok(obj.metadata.from === from && obj.metadata.sampleRate === 8000, 'listen: success maxLength metadata');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'listen-pause-resume\'', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let from = "listen_timeout";
let verbs = [
{
"verb": "listen",
"url": `ws://172.38.0.60:3000/${from}`,
"mixType" : "mixed"
}
];
provisionCallHook(from, verbs);
// THEN
const p = sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
await new Promise(r => setTimeout(r, 2000));
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}`);
const callSid = obj.body.call_sid;
// GIVEN
// Pause listen
let post = bent('http://127.0.0.1:3000/', 'POST', 202);
await post(`v1/updateCall/${callSid}`, {
"listen_status": "pause"
});
await new Promise(r => setTimeout(r, 2000));
// Resume listen
post = bent('http://127.0.0.1:3000/', 'POST', 202);
await post(`v1/updateCall/${callSid}`, {
"listen_status": "resume"
});
// turn off the call
post = bent('http://127.0.0.1:3000/', 'POST', 202);
await post(`v1/updateCall/${callSid}`, {
"call_status": "completed"
});
await p;
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,254 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook, provisionCustomHook} = require('./utils')
const bent = require('bent');
const getJSON = bent('json')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'play\' tests single link in plain text', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'https://example.com/example.mp3'
}
];
const from = 'play_single_link';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using single link');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests multi links in array', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: ['https://example.com/example.mp3', 'https://example.com/example.mp3']
}
];
const from = 'play_multi_links_in_array';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using links in array');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests single link in conference', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const from = 'play_single_link_in_conference';
const waitHookVerbs = [
{
verb: 'play',
url: 'https://example.com/example.mp3'
}
];
const verbs = [
{
verb: 'conference',
name: `${from}`,
beep: true,
"startConferenceOnEnter": false,
waitHook: `/customHook`
}
];
provisionCustomHook(from, waitHookVerbs)
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using in conference as single link');
// Make sure that waitHook is called and success
await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests multi links in array in conference', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const from = 'play_multi_links_in_conference';
const waitHookVerbs = [
{
verb: 'play',
url: ['https://example.com/example.mp3', 'https://example.com/example.mp3']
}
];
const verbs = [
{
verb: 'conference',
name: `${from}`,
beep: true,
"startConferenceOnEnter": false,
waitHook: `/customHook`
}
];
provisionCustomHook(from, waitHookVerbs)
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using in conference with multi links');
// Make sure that waitHook is called and success
await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests with seekOffset and actionHook', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'silence_stream://5000',
seekOffset: 8000,
timeoutSecs: 2,
actionHook: '/customHook'
}
];
const waitHookVerbs = [];
const from = 'play_action_hook';
provisionCallHook(from, verbs)
provisionCustomHook(from, waitHookVerbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds');
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`);
const seconds = parseInt(obj.body.playback_seconds);
const milliseconds = parseInt(obj.body.playback_milliseconds);
const lastOffsetPos = parseInt(obj.body.playback_last_offset_pos);
//console.log({obj}, 'lastRequest');
t.ok(obj.body.reason === "playCompleted", "play: actionHook success received");
t.ok(seconds === 2, "playback_seconds: actionHook success received");
t.ok(milliseconds === 2048, "playback_milliseconds: actionHook success received");
t.ok(lastOffsetPos > 15500 && lastOffsetPos < 16500, "playback_last_offset_pos: actionHook success received")
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests with earlymedia', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'silence_stream://5000',
earlyMedia: true
}
];
const from = 'play_early_media';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-invite-expect-183-cancel.xml', '172.38.0.10', from);
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_callStatus`);
t.ok(obj.body.sip_status === 487, "play: actionHook success received");
t.ok(obj.body.sip_reason === 'Request Terminated', "play: actionHook success received");
t.ok(obj.body.call_termination_by === 'caller', "play: actionHook success received");
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests with initial app_json', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
const from = 'play_initial_app_json';
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from, "16174000007");
t.pass('application can use app_json for initial instructions');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,7 +1,6 @@
const test = require('tape'); const test = require('tape');
const { sippUac } = require('./sipp')('test_fs'); const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module'); const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -21,21 +20,9 @@ test('\'say\' tests', async(t) => {
try { try {
await connect(srf); await connect(srf);
await sippUac('uac-say-account-creds-success.xml', '172.38.0.10');
// GIVEN
const verbs = [
{
verb: 'say',
text: 'hello'
}
];
const from = 'say_test_success';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('say: succeeds when using using account credentials'); t.pass('say: succeeds when using using account credentials');
disconnect(); disconnect();
} catch (err) { } catch (err) {
console.log(`error received: ${err}`); console.log(`error received: ${err}`);

View File

@@ -7,7 +7,7 @@
INVITE sip:16174000000@[remote_ip]:[remote_port] SIP/2.0 INVITE sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch] Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number] From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]> To: <sip:16174000000@[remote_ip]:[remote_port]>
Call-ID: [call_id] Call-ID: [call_id]
CSeq: 1 INVITE CSeq: 1 INVITE
@@ -41,7 +41,7 @@
ACK sip:16174000000@[remote_ip]:[remote_port] SIP/2.0 ACK sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
[last_Via] [last_Via]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number] From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>[peer_tag_param] To: <sip:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id] Call-ID: [call_id]
CSeq: 1 ACK CSeq: 1 ACK
@@ -53,3 +53,4 @@
</send> </send>
</scenario> </scenario>

View File

@@ -1,99 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-gather-account-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-gather-account-creds-success
Content-Length: 0
]]>
</send>
<nop>
<action>
<exec rtp_stream="/tmp/scenarios/wav/speak-to-customer-support.wav,1,0"/>
</action>
</nop>
<!-- Pause briefly -->
<pause milliseconds="3000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 3 BYE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>

View File

@@ -8,13 +8,13 @@
<send retrans="500"> <send retrans="500">
<![CDATA[ <![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0 INVITE sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch] Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number] From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]> To: <sip:16174000003@[remote_ip]:[remote_port]>
Call-ID: [call_id] Call-ID: [call_id]
CSeq: 1 INVITE CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port] Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70 Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-gather-account-creds-success Subject: uac-gather-account-creds-success
@@ -53,13 +53,13 @@
<send> <send>
<![CDATA[ <![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0 ACK sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch] Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number] From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param] To: 16174000003 <sip:16174000003@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id] Call-ID: [call_id]
CSeq: 1 ACK CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port] Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70 Max-Forwards: 70
Subject: uac-gather-account-creds-success Subject: uac-gather-account-creds-success
Content-Length: 0 Content-Length: 0

View File

@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" rtd="true">
<action>
<ereg regexp=";branch=[^;]*" search_in="hdr" header="Via" check_it="false" assign_to="1"/>
</action>
</recv>
<send>
<![CDATA[
CANCEL sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port][$1]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: [cseq] CANCEL
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
]]>
</send>
<recv response="200" rtd="true">
</recv>
<recv response="487" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port][$1]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -1,71 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="480" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -1,95 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-refer-no-notify.xml
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: REFER test with no NOT
Content-Length: 0
]]>
</send>
<!-- receive re-invite -->
<recv request="REFER" crlf="true"/>
<send>
<![CDATA[
SIP/2.0 202 Accepted
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: sip:sipp@[local_ip]:[local_port]
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
</scenario>

View File

@@ -1,115 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-refer-with-notify.xml
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-refer-with-notify.xml
Content-Length: 0
]]>
</send>
<!-- receive re-invite -->
<recv request="REFER" crlf="true"/>
<send>
<![CDATA[
SIP/2.0 202 Accepted
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: sip:sipp@[local_ip]:[local_port]
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<send retrans="500">
<![CDATA[
NOTIFY sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 NOTIFY
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-refer-with-notify.xml
Content-Type: message/sipfrag;version=2.0
Content-Length: 16
SIP/2.0 200 OK
]]>
</send>
<recv response="200"</recv>
</scenario>

View File

@@ -8,13 +8,13 @@
<send retrans="500"> <send retrans="500">
<![CDATA[ <![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0 INVITE sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch] Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number] From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]> To: <sip:16174000001@[remote_ip]:[remote_port]>
Call-ID: [call_id] Call-ID: [call_id]
CSeq: 1 INVITE CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port] Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70 Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say Subject: uac-say
@@ -53,13 +53,13 @@
<send> <send>
<![CDATA[ <![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0 ACK sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch] Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number] From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param] To: 16174000001 <sip:16174000001@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id] Call-ID: [call_id]
CSeq: 1 ACK CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port] Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70 Max-Forwards: 70
Subject: uac-say Subject: uac-say
Content-Length: 0 Content-Length: 0

View File

@@ -1,107 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-gather-account-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-gather-account-creds-success
Content-Length: 0
]]>
</send>
<recv request="INFO">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<pause milliseconds="3000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>

View File

@@ -1,164 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp default 'uas' scenario. -->
<!-- -->
<scenario name="Basic UAS responder">
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv request="INVITE" crlf="true">
<action>
<ereg regexp=".*" search_in="hdr" header="Subject:" assign_to="1" />
</action>
</recv>
<!-- The '[last_*]' keyword is replaced automatically by the -->
<!-- specified header if it was present in the last message received -->
<!-- (except if it was a retransmission). If the header was not -->
<!-- present or if no message has been received, the '[last_*]' -->
<!-- keyword is discarded, and all bytes until the end of the line -->
<!-- are also discarded. -->
<!-- -->
<!-- If the specified header was present several times in the -->
<!-- message, all occurrences are concatenated (CRLF separated) -->
<!-- to be used in place of the '[last_*]' keyword. -->
<send>
<![CDATA[
SIP/2.0 180 Ringing
[last_Via:]
[last_From:]
[last_To:];tag=[pid]SIPpTag01[call_number]
[last_Call-ID:]
[last_CSeq:]
[last_Record-Route:]
Subject:[$1]
Content-Length: 0
]]>
</send>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:];tag=[pid]SIPpTag01[call_number]
[last_Call-ID:]
[last_CSeq:]
[last_Record-Route:]
Subject:[$1]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv request="ACK"
rtd="true"
crlf="true">
</recv>
<recv request="INFO" optional="true" next="1">
</recv>
<recv request="INVITE" crlf="true">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:];tag=[pid]SIPpTag01[call_number]
[last_Call-ID:]
[last_CSeq:]
[last_Record-Route:]
Subject:[$1]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv request="ACK"
rtd="true"
crlf="true">
</recv>
<recv request="BYE">
</recv>
<send next="2">
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
<label id="1"/>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
<label id="2"/>
</scenario>

View File

@@ -1,99 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp default 'uas' scenario. -->
<!-- -->
<scenario name="UAS Timeout Receive Cancel">
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv request="INVITE" crlf="true">
<action>
<ereg regexp=".*" search_in="hdr" header="Subject:" assign_to="1" />
<ereg regexp=".*" search_in="hdr" header="CSeq:" check_it="false" assign_to="2"/>
</action>
</recv>
<!-- The '[last_*]' keyword is replaced automatically by the -->
<!-- specified header if it was present in the last message received -->
<!-- (except if it was a retransmission). If the header was not -->
<!-- present or if no message has been received, the '[last_*]' -->
<!-- keyword is discarded, and all bytes until the end of the line -->
<!-- are also discarded. -->
<!-- -->
<!-- If the specified header was present several times in the -->
<!-- message, all occurrences are concatenated (CRLF separated) -->
<!-- to be used in place of the '[last_*]' keyword. -->
<send>
<![CDATA[
SIP/2.0 180 Ringing
[last_Via:]
[last_From:]
[last_To:];tag=[pid]SIPpTag01[call_number]
[last_Call-ID:]
[last_CSeq:]
[last_Record-Route:]
Subject:[$1]
Content-Length: 0
]]>
</send>
<recv request="CANCEL" >
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:];tag=[pid]SIPpTag01[call_number]
[last_Call-ID:]
[last_CSeq:]
Subject:[$1]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
]]>
</send>
<send>
<![CDATA[
SIP/2.0 487 Request Terminated
[last_Via:]
[last_From:]
[last_To:];tag=[pid]SIPpTag01[call_number]
[last_Call-ID:]
CSeq: [$2]
[last_Record-Route:]
Subject:[$1]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
]]>
</send>
<recv request="ACK"
rtd="true"
crlf="true">
</recv>
</scenario>

View File

@@ -1,100 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook, provisionCustomHook, provisionActionHook} = require('./utils')
const bent = require('bent');
const getJSON = bent('json')
const sleepFor = async(ms) => new Promise(resolve => setTimeout(resolve, ms));
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'refer\' tests w/202 and NOTIFY', {timeout: 25000}, async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'say',
text: 'silence_stream://100'
},
{
verb: 'sip:refer',
referTo: '123456',
actionHook: '/actionHook'
}
];
const noVerbs = [];
const from = 'refer_with_notify';
provisionCallHook(from, verbs);
provisionActionHook(from, noVerbs)
// THEN
await sippUac('uac-refer-with-notify.xml', '172.38.0.10', from);
t.pass('refer: successfully received 202 Accepted');
await sleepFor(1000);
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.final_referred_call_status === 200, 'refer: successfully received NOTIFY with 200 OK');
//console.log(`obj: ${JSON.stringify(obj)}`);
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'refer\' tests w/202 but no NOTIFY', {timeout: 25000}, async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'say',
text: 'silence_stream://100'
},
{
verb: 'sip:refer',
referTo: '123456',
actionHook: '/actionHook'
}
];
const noVerbs = [];
const from = 'refer_no_notify';
provisionCallHook(from, verbs);
provisionActionHook(from, noVerbs)
// THEN
await sippUac('uac-refer-no-notify.xml', '172.38.0.10', from);
t.pass('refer: successfully received 202 Accepted w/o NOTIFY');
await sleepFor(17000);
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
console.log(`obj: ${JSON.stringify(obj)}`);
t.ok(obj.body.refer_status === 202, 'refer: successfully timed out and reported 202');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,55 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('sending SIP in-dialog requests tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
//GIVEN
let verbs = [
{
"verb": "say",
"text": "hello"
},
{
"verb": "sip:request",
"method": "info",
"headers": {
"Content-Type": "application/text"
},
"body": "here I am ",
"actionHook": "/actionHook"
}
];
let from = "sip_indialog_test";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10', from);
const obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.sip_status === 200, 'successfully sent SIP INFO');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,5 +1,7 @@
const test = require('tape'); const test = require('blue-tape');
const { sippUac } = require('./sipp')('test_sbc-inbound'); const { output, sippUac } = require('./sipp')('test_sbc-inbound');
const debug = require('debug')('drachtio:sbc-inbound');
const clearModule = require('clear-module');
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -53,13 +55,6 @@ test('incoming call tests', (t) => {
.then(() => { .then(() => {
return t.pass('handles in-dialog requests'); return t.pass('handles in-dialog requests');
}) })
.then(() => {
return sippUac('uac-refer-no-notify.xml', '172.38.0.30');
})
.then(() => {
return t.pass('handles sip:refer where we get 202 but no NOTIFY');
})
.then(() => { .then(() => {
srf.disconnect(); srf.disconnect();
t.end(); t.end();

View File

@@ -24,24 +24,22 @@ obj.output = () => {
return output; return output;
}; };
obj.sippUac = (file, bindAddress, from='sipp', to='16174000000', loop=1) => { obj.sippUac = (file, bindAddress) => {
const cmd = 'docker'; const cmd = 'docker';
const args = [ const args = [
'run', '-t', '--rm', '--net', `${network}`, 'run', '-t', '--rm', '--net', `${network}`,
'-v', `${__dirname}/scenarios:/tmp/scenarios`, '-v', `${__dirname}/scenarios:/tmp/scenarios`,
'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`, 'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
'-m', loop, '-m', '1',
'-sleep', '250ms', '-sleep', '250ms',
'-nostdin', '-nostdin',
'-cid_str', `%u-%p@%s-${idx++}`, '-cid_str', `%u-%p@%s-${idx++}`,
'172.38.0.50', '172.38.0.50'
'-key','from', from,
'-key','to', to, '-trace_msg'
]; ];
if (bindAddress) args.splice(5, 0, '--ip', bindAddress); if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
//console.log(args.join(' ')); console.log(args.join(' '));
clearOutput(); clearOutput();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -63,7 +61,7 @@ obj.sippUac = (file, bindAddress, from='sipp', to='16174000000', loop=1) => {
addOutput(data.toString()); addOutput(data.toString());
}); });
child_process.stdout.on('data', (data) => { child_process.stdout.on('data', (data) => {
// console.log(`stdout: ${data}`); //console.log(`stdout: ${data}`);
addOutput(data.toString()); addOutput(data.toString());
}); });
}); });

View File

@@ -1,15 +0,0 @@
[
{
"verb": "say",
"text": "hello"
},
{
"verb": "sip:request",
"method": "info",
"headers": {
"Content-Type": "application/text"
},
"body": "here I am ",
"actionHook": "/actionHook"
}
]

View File

@@ -1,210 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'transcribe\' test - google', async(t) => {
if (!process.env.GCP_JSON_KEY) {
t.pass('skipping google tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "transcribe",
"recognizer": {
"vendor": "google",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"transcriptionHook": "/transcriptionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using google credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - microsoft', async(t) => {
if (!process.env.MICROSOFT_REGION || !process.env.MICROSOFT_API_KEY) {
t.pass('skipping microsoft tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "transcribe",
"recognizer": {
"vendor": "microsoft",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"transcriptionHook": "/transcriptionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using microsoft credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - aws', async(t) => {
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
t.pass('skipping aws tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "transcribe",
"recognizer": {
"vendor": "aws",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"transcriptionHook": "/transcriptionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using aws credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - deepgram', async(t) => {
if (!process.env.DEEPGRAM_API_KEY ) {
t.pass('skipping deepgram tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "transcribe",
"recognizer": {
"vendor": "deepgram",
"hints": ["customer support", "sales", "human resources", "HR"],
"deepgramOptions": {
"apiKey": process.env.DEEPGRAM_API_KEY
}
},
"transcriptionHook": "/transcriptionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using deepgram credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - soniox', async(t) => {
if (!process.env.SONIOX_API_KEY ) {
t.pass('skipping soniox tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "transcribe",
"recognizer": {
"vendor": "soniox",
"hints": ["customer support", "sales", "human resources", "HR"],
"deepgramOptions": {
"apiKey": process.env.SONIOX_API_KEY
}
},
"transcriptionHook": "/transcriptionHook"
}
];
let from = "gather_success";
provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using soniox credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -44,25 +44,10 @@ test('unit tests', (t) => {
task = makeTask(logger, require('./data/good/say-text-array')); task = makeTask(logger, require('./data/good/say-text-array'));
t.ok(task.name === 'say', 'parsed say with multiple segments'); t.ok(task.name === 'say', 'parsed say with multiple segments');
task = makeTask(logger, require('./data/good/say-ssml'));
// the ssml is more than 1000 chars,
// expecting first chunk is length > 100, stop at ? instead of first .
// 2nd chunk is long text < 1000 char, stop at .
// 3rd chunk is the rest.
t.ok(task.text.length === 3 &&
task.text[0].length === 187 &&
task.text[1].length === 882 &&
task.text[2].length === 123, 'parsed say');
task = makeTask(logger, require('./data/bad/bad-say-ssml'));
t.ok(task.text.length === 1 &&
task.text[0].length === 1162, 'parsed bad say');
const alt = require('./data/good/alternate-syntax'); const alt = require('./data/good/alternate-syntax');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalize = require('../lib/utils/normalize-jambones');
normalizeJambones(logger, alt).forEach((t) => { normalize(logger, alt).forEach((t) => {
const task = makeTask(logger, t); const task = makeTask(logger, t);
}); });
t.pass('alternate syntax works'); t.pass('alternate syntax works');
@@ -77,4 +62,4 @@ const errMissingProperty = () => makeTask(logger, require('./data/bad/missing-re
const errInvalidType = () => makeTask(logger, require('./data/bad/invalid-type')); const errInvalidType = () => makeTask(logger, require('./data/bad/invalid-type'));
const errBadEnum = () => makeTask(logger, require('./data/bad/bad-enum')); const errBadEnum = () => makeTask(logger, require('./data/bad/bad-enum'));
const errBadPayload = () => makeTask(logger, require('./data/bad/bad-payload')); const errBadPayload = () => makeTask(logger, require('./data/bad/bad-payload'));
const errBadPayload2 = () => makeTask(logger, require('./data/bad/bad-payload2')); const errBadPayload2 = () => makeTask(logger, require('./data/bad/bad-payload2'));

View File

@@ -1,36 +0,0 @@
const bent = require('bent');
/*
* phoneNumber: 16174000000
* Hook endpoints http://127.0.0.1:3100/
* The function help testcase to register desired jambonz json response for an application call
* When a call has From number match the registered hook event, the desired jambonz json will be responded.
*/
const provisionCallHook = (from, verbs) => {
const mapping = {
from,
data: JSON.stringify(verbs)
};
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
post('/appMapping', mapping);
}
const provisionCustomHook = (from, verbs) => {
const mapping = {
from,
data: JSON.stringify(verbs)
};
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
post(`/customHookMapping`, mapping);
}
const provisionActionHook = (from, verbs) => {
const mapping = {
from,
data: JSON.stringify(verbs)
};
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
post(`/actionHook`, mapping);
}
module.exports = { provisionCallHook, provisionCustomHook, provisionActionHook}

View File

@@ -1,23 +1,15 @@
FROM --platform=linux/amd64 node:18.6.0-alpine as base FROM node:alpine as builder
RUN apk update && apk add --no-cache python make g++
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/ WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
FROM base as build FROM node:alpine as webapp
RUN apk add curl
COPY package.json package-lock.json ./ WORKDIR /opt/app
COPY . /opt/app
RUN npm ci COPY --from=builder /opt/app/node_modules ./node_modules
COPY ./entrypoint.sh /entrypoint.sh
COPY . . RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
FROM base
COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ]

View File

@@ -1,190 +1,49 @@
const assert = require('assert');
const fs = require('fs');
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const Websocket = require('ws');
const listenPort = process.env.HTTP_PORT || 3000; const listenPort = process.env.HTTP_PORT || 3000;
let json_mapping = new Map(); let lastAction, lastEvent;
let hook_mapping = new Map();
let ws_packet_count = new Map();
let ws_metadata = new Map();
/** websocket server for listen audio */ assert.ok(process.env.APP_PATH, 'env var APP_PATH is required');
const recvAudio = (socket, req) => {
let packets = 0;
let path = req.url;
console.log('received websocket connection');
socket.on('message', (data, isBinary) => {
if (!isBinary) {
try {
const msg = JSON.parse(data);
console.log({msg}, 'received websocket message');
ws_metadata.set(path, msg);
}
catch (err) {
console.log({err}, 'error parsing websocket message');
}
}
else {
packets += data.length;
}
});
socket.on('error', (err) => {
console.log({err}, 'listen websocket: error');
});
socket.on('close', () => { app.listen(listenPort, () => {
ws_packet_count.set(path, packets);
})
};
const wsServer = new Websocket.Server({ noServer: true });
wsServer.setMaxListeners(0);
wsServer.on('connection', recvAudio.bind(null));
const server = app.listen(listenPort, () => {
console.log(`sample jambones app server listening on ${listenPort}`); console.log(`sample jambones app server listening on ${listenPort}`);
}); });
server.on('upgrade', (request, socket, head) => {
console.log('received upgrade request');
wsServer.handleUpgrade(request, socket, head, (socket) => {
wsServer.emit('connection', socket, request);
});
});
const applicationData = JSON.parse(fs.readFileSync(process.env.APP_PATH));
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
/*
* Markup language
*/
app.all('/', (req, res) => { app.all('/', (req, res) => {
console.log(req.body, 'POST /'); console.log(applicationData, `${req.method} /`);
const key = req.body.from return res.json(applicationData);
addRequestToMap(key, req, hook_mapping);
return getJsonFromMap(key, req, res);
}); });
app.post('/appMapping', (req, res) => {
console.log(req.body, 'POST /appMapping');
json_mapping.set(req.body.from, req.body.data);
return res.sendStatus(200);
});
/*
* Status Callback
*/
app.post('/callStatus', (req, res) => { app.post('/callStatus', (req, res) => {
console.log({payload: req.body}, 'POST /callStatus'); console.log({payload: req.body}, 'POST /callStatus');
let key = req.body.from + "_callStatus"
addRequestToMap(key, req, hook_mapping);
return res.sendStatus(200); return res.sendStatus(200);
}); });
/*
* transcriptionHook
*/
app.post('/transcriptionHook', (req, res) => {
console.log({payload: req.body}, 'POST /transcriptionHook');
let key = req.body.from + "_actionHook"
addRequestToMap(key, req, hook_mapping);
return res.json([{"verb": "hangup"}]);
});
/*
* actionHook
*/
app.post('/actionHook', (req, res) => { app.post('/actionHook', (req, res) => {
console.log({payload: req.body}, 'POST /actionHook'); console.log({payload: req.body}, 'POST /actionHook');
let key = req.body.from + "_actionHook" lastAction = req.body;
addRequestToMap(key, req, hook_mapping);
return res.sendStatus(200); return res.sendStatus(200);
}); });
/* app.get('/actionHook', (req, res) => {
* customHook console.log({payload: lastAction}, 'GET /actionHook');
* For the hook to return return res.json(lastAction);
*/
app.all('/customHook', (req, res) => {
let key = `${req.body.from}_customHook`;;
console.log(req.body, `POST /customHook`);
return getJsonFromMap(key, req, res);
}); });
app.post('/customHookMapping', (req, res) => { app.post('/eventHook', (req, res) => {
let key = `${req.body.from}_customHook`; console.log({payload: req.body}, 'POST /eventHook');
console.log(req.body, `POST /customHookMapping`); lastEvent = req.body;
json_mapping.set(key, req.body.data);
return res.sendStatus(200); return res.sendStatus(200);
}); });
// Fetch Requests app.get('/eventHook', (req, res) => {
app.get('/requests/:key', (req, res) => { console.log({payload: lastEvent}, 'GET /eventHook');
let key = req.params.key; return res.json(lastEvent);
if (hook_mapping.has(key)) { });
return res.json(hook_mapping.get(key));
} else {
return res.sendStatus(404);
}
})
app.get('/lastRequest/:key', (req, res) => {
let key = req.params.key;
if (hook_mapping.has(key)) {
let requests = hook_mapping.get(key);
return res.json(requests[requests.length - 1]);
} else {
return res.sendStatus(404);
}
})
// WS Fetch
app.get('/ws_packet_count/:key', (req, res) => {
let key = `/${req.params.key}`;
console.log(key, ws_packet_count);
if (ws_packet_count.has(key)) {
return res.json({ count: ws_packet_count.get(key) });
} else {
return res.sendStatus(404);
}
})
app.get('/ws_metadata/:key', (req, res) => {
let key = `/${req.params.key}`;
console.log(key, ws_packet_count);
if (ws_metadata.has(key)) {
return res.json({ metadata: ws_metadata.get(key) });
} else {
return res.sendStatus(404);
}
})
/*
* private function
*/
function getJsonFromMap(key, req, res) {
if (!json_mapping.has(key)) return res.sendStatus(404);
const retData = JSON.parse(json_mapping.get(key));
console.log(retData, ` Response to ${req.method} ${req.url}`);
addRequestToMap(key, req, hook_mapping);
return res.json(retData);
}
function addRequestToMap(key, req, map) {
let headers = new Map()
for(let i = 0; i < req.rawHeaders.length; i++) {
if (i % 2 === 0) {
headers.set(req.rawHeaders[i], req.rawHeaders[i + 1])
}
}
let request = {
'url': req.url,
'headers': Object.fromEntries(headers),
'body': req.body
}
if (map.has(key)) {
map.get(key).push(request);
} else {
map.set(key, [request]);
}
}

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd /opt/app/
npm start

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@
"author": "Dave Horton", "author": "Dave Horton",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.17.1"
"ws": "^8.12.0"
} }
} }

View File

@@ -1,15 +1,6 @@
const test = require('tape'); const test = require('tape');
const { sippUac } = require('./sipp')('test_fs'); const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module'); const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils')
const opts = {
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const logger = require('pino')(opts);
const { queryAlerts } = require('@jambonz/time-series')(
logger, process.env.JAMBONES_TIME_SERIES_HOST
);
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -29,21 +20,7 @@ test('basic webhook tests', async(t) => {
try { try {
await connect(srf); await connect(srf);
const verbs = [ await sippUac('uac-expect-603.xml', '172.38.0.10');
{
verb: 'sip:decline',
status: 603,
reason: 'Gone Fishin',
headers: {
'Retry-After': 300
}
}
];
const from = 'sip_decline_test_success';
provisionCallHook(from, verbs)
await sippUac('uac-expect-603.xml', '172.38.0.10', from);
t.pass('webhook successfully declines call'); t.pass('webhook successfully declines call');
disconnect(); disconnect();
@@ -53,43 +30,3 @@ test('basic webhook tests', async(t) => {
t.error(err); t.error(err);
} }
}); });
test('invalid jambonz json create alert tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
// Invalid json array
const verbs = {
verb: 'say',
text: 'hello'
};
const from = 'invalid_json_create_alert';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-invite-expect-480.xml', '172.38.0.10', from);
// sleep testcase for more than 7 second to wait alert pushed to database.
await sleep(8000);
const data = await queryAlerts(
{account_sid: 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f', page: 1, page_size: 25, days: 7});
let checked = false;
for (let i = 0; i < data.total; i++) {
checked = data.data[i].message === 'malformed jambonz payload: must be array'
}
t.ok(checked, 'alert is raised as expected');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -1,97 +0,0 @@
class MockWebsocket {
static eventResponses = new Map();
static actionLoops = new Map();
eventListeners = new Map();
constructor(url, protocols, options) {
this.u = url;
this.pros = protocols;
this.opts = options;
setTimeout(() => {
this.open();
}, 500)
}
static addJsonMapping(key, value) {
MockWebsocket.eventResponses.set(key, value);
}
static getAndIncreaseActionLoops(key) {
const ret = MockWebsocket.actionLoops.has(key) ? MockWebsocket.actionLoops.get(key) : 0;
MockWebsocket.actionLoops.set(key, ret + 1);
return ret;
}
once(event, listener) {
// Websocket.ws = this;
this.eventListeners.set(event, listener);
return this;
}
on(event, listener) {
// Websocket.ws = this;
this.eventListeners.set(event, listener);
return this;
}
open() {
if (this.eventListeners.has('open')) {
this.eventListeners.get('open')();
}
}
removeAllListeners() {
this.eventListeners.clear();
}
send(data, callback) {
const json = JSON.parse(data);
console.log({json}, 'got message from ws-requestor');
if (MockWebsocket.eventResponses.has(json.call_sid)) {
const resp_data = MockWebsocket.eventResponses.get(json.call_sid);
const action = resp_data.action[MockWebsocket.getAndIncreaseActionLoops(json.call_sid)];
if (action === 'connect') {
setTimeout(()=> {
const msg = {
type: 'ack',
msgid: json.msgid,
command: 'command',
call_sid: json.call_sid,
queueCommand: false,
data: resp_data.body}
console.log({msg}, 'sending ack to ws-requestor');
this.mockOnMessage(JSON.stringify(msg));
}, 100);
} else if (action === 'close') {
if (this.eventListeners.has('close')) {
this.eventListeners.get('close')(500);
}
} else if (action === 'terminate') {
if (this.eventListeners.has('close')) {
this.eventListeners.get('close')(1000);
}
} else if (action === 'error') {
if (this.eventListeners.has('error')) {
this.eventListeners.get('error')();
}
} else if (action === 'unexpected-response') {
if (this.eventListeners.has('unexpected-response')) {
this.eventListeners.get('unexpected-response')();
}
}
}
if (callback) {
callback();
}
}
mockOnMessage(message, isBinary=false) {
if (this.eventListeners.has('message')) {
this.eventListeners.get('message')(message, isBinary);
}
}
}
module.exports = MockWebsocket;

Some files were not shown because too many files have changed in this diff Show More