mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
91 Commits
v0.8.0-rc6
...
testbuild-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3ff42987 | ||
|
|
765311c491 | ||
|
|
dcd8b378b2 | ||
|
|
552a4e9fd1 | ||
|
|
04003a709e | ||
|
|
565ee609ef | ||
|
|
9587465e85 | ||
|
|
845d80a23d | ||
|
|
3109db7861 | ||
|
|
11c5047465 | ||
|
|
e19ea629f0 | ||
|
|
fe529c6bfb | ||
|
|
e980b82ec4 | ||
|
|
318ca19791 | ||
|
|
e2bd211346 | ||
|
|
410c07fae6 | ||
|
|
2ebfbfb3d8 | ||
|
|
a29795839d | ||
|
|
28088a4cdd | ||
|
|
afb381eec9 | ||
|
|
ed00ccb681 | ||
|
|
6e945dde9a | ||
|
|
efdea3e514 | ||
|
|
5131d524ce | ||
|
|
c0114015ea | ||
|
|
a293ec09d0 | ||
|
|
f71ae83ce4 | ||
|
|
0dd161913c | ||
|
|
63ab554908 | ||
|
|
e1bd075ebc | ||
|
|
9de89258a1 | ||
|
|
145ed488db | ||
|
|
c06a43adfa | ||
|
|
bebc82d194 | ||
|
|
cdc82e99ff | ||
|
|
dd4d9aa261 | ||
|
|
1dcf9ee5a2 | ||
|
|
4b28db0946 | ||
|
|
e7ff76b938 | ||
|
|
f245275983 | ||
|
|
690deed89d | ||
|
|
26053ec709 | ||
|
|
34e8203338 | ||
|
|
7be3c64116 | ||
|
|
f71d3aed8b | ||
|
|
5ab24337b2 | ||
|
|
2af76d94a6 | ||
|
|
4919c05181 | ||
|
|
3084a9d6ba | ||
|
|
1c683f1142 | ||
|
|
ab1947e23e | ||
|
|
5527abff09 | ||
|
|
68827112fc | ||
|
|
8a9a2df128 | ||
|
|
3a3544a5e8 | ||
|
|
cbeb706946 | ||
|
|
f005262615 | ||
|
|
67ec28484c | ||
|
|
803a944240 | ||
|
|
a5cd342e46 | ||
|
|
e91feb64f5 | ||
|
|
ae688ddc7e | ||
|
|
9b21b65478 | ||
|
|
c09425fa89 | ||
|
|
6706992b4b | ||
|
|
0fdcb3a6d6 | ||
|
|
50057deca9 | ||
|
|
c7eacdd0f8 | ||
|
|
e990b5dbf9 | ||
|
|
7ae37b1e60 | ||
|
|
ed284c367d | ||
|
|
272380dc62 | ||
|
|
61dbb659b3 | ||
|
|
fbae7c0eab | ||
|
|
4894a85569 | ||
|
|
0e5bb876ce | ||
|
|
8658d03f1f | ||
|
|
f2ff5250b0 | ||
|
|
c37fba541f | ||
|
|
f9921cf4e9 | ||
|
|
86fed4ec90 | ||
|
|
9d07a1354c | ||
|
|
2775c7ddd1 | ||
|
|
70822cb278 | ||
|
|
14a02735be | ||
|
|
4b3ebe37ac | ||
|
|
f4fbd07f8e | ||
|
|
6ebba8673f | ||
|
|
2b06177dc5 | ||
|
|
088316d266 | ||
|
|
8c0044a378 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
54
.github/workflows/docker-publish.yml
vendored
54
.github/workflows/docker-publish.yml
vendored
@@ -2,13 +2,8 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE_NAME: feature-server
|
||||
@@ -20,32 +15,37 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
- name: prepare tag
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
IMAGE_ID=jambonz/$IMAGE_NAME
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ env.IMAGE_ID }}:${{ env.VERSION }}
|
||||
build-args: |
|
||||
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
|
||||
GITHUB_REF=$GITHUB_REF
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:18.12.1-alpine3.16 as base
|
||||
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@ Configuration is provided via environment variables:
|
||||
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|
||||
|DRACHTIO_SECRET| shared secret|yes|
|
||||
|ENABLE_METRICS| if 1, metrics will be generated|no|
|
||||
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |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|
|
||||
|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_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|
||||
|JAMBONES_MYSQL_HOST| mysql host|yes|
|
||||
|
||||
20
app.js
20
app.js
@@ -8,6 +8,7 @@ assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
||||
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
|
||||
assert.ok(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
|
||||
|
||||
const Srf = require('drachtio-srf');
|
||||
const srf = new Srf();
|
||||
@@ -106,16 +107,25 @@ const disconnect = () => {
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGUSR2', handle);
|
||||
process.on('SIGTERM', handle);
|
||||
|
||||
function handle(signal) {
|
||||
const {removeFromSet} = srf.locals.dbHelpers;
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
|
||||
removeFromSet(setName, srf.locals.localSipAddress);
|
||||
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
|
||||
srf.locals.disabled = true;
|
||||
logger.info(`got signal ${signal}`);
|
||||
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}`);
|
||||
removeFromSet(setName, srf.locals.localSipAddress);
|
||||
}
|
||||
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
|
||||
if (process.env.K8S) {
|
||||
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) {
|
||||
|
||||
@@ -104,7 +104,7 @@ router.post('/', async(req, res) => {
|
||||
proxy: `sip:${sbcAddress}`,
|
||||
localSdp: ep.local.sdp
|
||||
});
|
||||
if (target.auth) opts.auth = this.target.auth;
|
||||
if (target.auth) opts.auth = target.auth;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ const router = require('express').Router();
|
||||
const CallInfo = require('../../session/call-info');
|
||||
const {CallDirection} = require('../../utils/constants');
|
||||
const SmsSession = require('../../session/sms-call-session');
|
||||
const normalizeJambones = require('../../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const makeTask = require('../../tasks/make_task');
|
||||
|
||||
router.post('/:sid', async(req, res) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ const WsRequestor = require('../../utils/ws-requestor');
|
||||
const CallInfo = require('../../session/call-info');
|
||||
const {CallDirection} = require('../../utils/constants');
|
||||
const SmsSession = require('../../session/sms-call-session');
|
||||
const normalizeJambones = require('../../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const {TaskPreconditions} = require('../../utils/constants');
|
||||
const makeTask = require('../../tasks/make_task');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const HttpRequestor = require('./utils/http-requestor');
|
||||
const WsRequestor = require('./utils/ws-requestor');
|
||||
const makeTask = require('./tasks/make_task');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const normalizeJambones = require('./utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const dbUtils = require('./utils/db-utils');
|
||||
const RootSpan = require('./utils/call-tracer');
|
||||
const listTaskNames = require('./utils/summarize-tasks');
|
||||
@@ -27,7 +27,11 @@ module.exports = function(srf, logger) {
|
||||
|
||||
function initLocals(req, res, next) {
|
||||
const callId = req.get('Call-ID');
|
||||
logger.info({callId}, 'new incoming call');
|
||||
logger.info({
|
||||
callId,
|
||||
callingNumber: req.callingNumber,
|
||||
calledNumber: req.calledNumber
|
||||
}, 'new incoming call');
|
||||
if (!req.has('X-Account-Sid')) {
|
||||
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
|
||||
return res.send(500);
|
||||
@@ -42,7 +46,16 @@ module.exports = function(srf, logger) {
|
||||
}
|
||||
if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User');
|
||||
if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN');
|
||||
|
||||
if (req.has('X-Cisco-Recording-Participant')) {
|
||||
const ciscoParticipants = req.get('X-Cisco-Recording-Participant');
|
||||
const regex = /sip:[\d]+@[\d]+\.[\d]+\.[\d]+\.[\d]+/g;
|
||||
const sipURIs = ciscoParticipants.match(regex);
|
||||
logger.info(`X-Cisco-Recording-Participant : ${sipURIs} `);
|
||||
if (sipURIs && sipURIs.length > 0) {
|
||||
req.locals.calledNumber = sipURIs[0];
|
||||
req.locals.callingNumber = sipURIs[1];
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -90,8 +103,10 @@ module.exports = function(srf, logger) {
|
||||
.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;
|
||||
if (!req.locals.calledNumber && !req.locals.calledNumber) {
|
||||
req.locals.calledNumber = metadata.caller.number;
|
||||
req.locals.callingNumber = metadata.callee.number;
|
||||
}
|
||||
req.locals = {
|
||||
...req.locals,
|
||||
siprec: {
|
||||
@@ -276,34 +291,39 @@ module.exports = function(srf, logger) {
|
||||
return next();
|
||||
}
|
||||
/* retrieve the application to execute for this inbound call */
|
||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
|
||||
req.locals.callInfo,
|
||||
{service_provider_sid: req.locals.service_provider_sid},
|
||||
{
|
||||
defaults: {
|
||||
synthesizer: {
|
||||
vendor: app.speech_synthesis_vendor,
|
||||
language: app.speech_synthesis_language,
|
||||
voice: app.speech_synthesis_voice
|
||||
},
|
||||
recognizer: {
|
||||
vendor: app.speech_recognizer_vendor,
|
||||
language: app.speech_recognizer_language
|
||||
let json;
|
||||
if (app.app_json) {
|
||||
json = JSON.parse(app.app_json);
|
||||
} else {
|
||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {},
|
||||
req.locals.callInfo,
|
||||
{ service_provider_sid: req.locals.service_provider_sid },
|
||||
{
|
||||
defaults: {
|
||||
synthesizer: {
|
||||
vendor: app.speech_synthesis_vendor,
|
||||
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');
|
||||
span = obj.span;
|
||||
const b3 = rootSpan.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
||||
});
|
||||
logger.debug({ params }, 'sending initial webhook');
|
||||
const obj = rootSpan.startChildSpan('performAppWebhook');
|
||||
span = obj.span;
|
||||
const b3 = rootSpan.getTracingPropagation();
|
||||
const httpHeaders = b3 && { b3 };
|
||||
json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
||||
}
|
||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
span.setAttributes({
|
||||
span?.setAttributes({
|
||||
'http.statusCode': 200,
|
||||
'app.tasks': listTaskNames(app.tasks)
|
||||
});
|
||||
span.end();
|
||||
span?.end();
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
|
||||
if (siprec) {
|
||||
|
||||
@@ -33,6 +33,23 @@ class CallInfo {
|
||||
this.callStatus = CallStatus.Trying;
|
||||
this.originatingSipIp = req.get('X-Forwarded-For');
|
||||
this.originatingSipTrunkName = req.get('X-Originating-Carrier');
|
||||
const {siprec} = req.locals;
|
||||
if (siprec) {
|
||||
const caller = parseUri(req.locals.callingNumber);
|
||||
const callee = parseUri(req.locals.calledNumber);
|
||||
this.participants = [
|
||||
{
|
||||
participant: 'caller',
|
||||
uriUser: caller.user,
|
||||
uriHost: caller.host
|
||||
},
|
||||
{
|
||||
participant: 'callee',
|
||||
uriUser: callee.user,
|
||||
uriHost: callee.host
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
else if (opts.parentCallInfo) {
|
||||
// outbound call that is a child of an existing call
|
||||
|
||||
@@ -13,7 +13,7 @@ const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
const sessionTracker = require('./session-tracker');
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const listTaskNames = require('../utils/summarize-tasks');
|
||||
const HttpRequestor = require('../utils/http-requestor');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
@@ -270,6 +270,10 @@ class CallSession extends Emitter {
|
||||
return this.backgroundGatherTask;
|
||||
}
|
||||
|
||||
get isListenEnabled() {
|
||||
return this.backgroundListenTask;
|
||||
}
|
||||
|
||||
get b3() {
|
||||
return this.rootSpan?.getTracingPropagation();
|
||||
}
|
||||
@@ -461,14 +465,70 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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]);
|
||||
this.backgroundGatherTask = makeTask(this.logger, t[0]);
|
||||
const task = makeTask(this.logger, t[0]);
|
||||
|
||||
if (this.isBotModeEnabled) {
|
||||
const currInput = this.backgroundGatherTask.input;
|
||||
const newInput = task.input;
|
||||
if (JSON.stringify(currInput) === JSON.stringify(newInput)) {
|
||||
this.logger.info('CallSession:enableBotMode - bot mode currently enabled, ignoring request to start again');
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.logger.info({currInput, newInput},
|
||||
'CallSession:enableBotMode - restarting background gather to apply new input type');
|
||||
this.backgroundGatherTask.sticky = false;
|
||||
this.disableBotMode();
|
||||
}
|
||||
}
|
||||
this.backgroundGatherTask = task;
|
||||
this._bargeInEnabled = true;
|
||||
this.backgroundGatherTask
|
||||
.once('dtmf', this._clearTasks.bind(this, this.backgroundGatherTask))
|
||||
@@ -480,13 +540,15 @@ class CallSession extends Emitter {
|
||||
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`);
|
||||
this.backgroundGatherTask.span = span;
|
||||
this.backgroundGatherTask.ctx = ctx;
|
||||
this.backgroundGatherTask.sticky = autoEnable;
|
||||
this.backgroundGatherTask.exec(this, resources)
|
||||
.then(() => {
|
||||
this.logger.info('CallSession:enableBotMode: gather completed');
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.span.end();
|
||||
const sticky = this.backgroundGatherTask?.sticky;
|
||||
this.backgroundGatherTask = null;
|
||||
if (autoEnable && !this.callGone && !this._stopping && this._bargeInEnabled) {
|
||||
if (sticky && !this.callGone && !this._stopping && this._bargeInEnabled) {
|
||||
this.logger.info('CallSession:enableBotMode: restarting background gather');
|
||||
setImmediate(() => this.enableBotMode(gather, true));
|
||||
}
|
||||
@@ -588,7 +650,9 @@ class CallSession extends Emitter {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
client_id: credential.client_id,
|
||||
secret: credential.secret
|
||||
secret: credential.secret,
|
||||
nuance_tts_uri: credential.nuance_tts_uri,
|
||||
nuance_stt_uri: credential.nuance_stt_uri
|
||||
};
|
||||
}
|
||||
else if ('deepgram' === vendor) {
|
||||
@@ -597,6 +661,12 @@ class CallSession extends Emitter {
|
||||
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,
|
||||
@@ -606,6 +676,14 @@ class CallSession extends Emitter {
|
||||
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 {
|
||||
writeAlerts({
|
||||
@@ -636,7 +714,7 @@ class CallSession extends Emitter {
|
||||
let skip = false;
|
||||
this.currentTask = task;
|
||||
if (TaskName.Gather === task.name && this.isBotModeEnabled) {
|
||||
if (this.backgroundGatherTask.updateTaskInProgress(task)) {
|
||||
if (this.backgroundGatherTask.updateTaskInProgress(task) !== false) {
|
||||
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
|
||||
skip = true;
|
||||
}
|
||||
@@ -700,7 +778,6 @@ class CallSession extends Emitter {
|
||||
|
||||
trackTmpFile(path) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -1075,14 +1152,14 @@ class CallSession extends Emitter {
|
||||
_injectTasks(newTasks) {
|
||||
const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather);
|
||||
const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather;
|
||||
|
||||
/*
|
||||
this.logger.debug({
|
||||
currentTaskList: listTaskNames(this.tasks),
|
||||
newContent: listTaskNames(newTasks),
|
||||
currentlyExecutingGather,
|
||||
gatherPos
|
||||
}, 'CallSession:_injectTasks - starting');
|
||||
|
||||
*/
|
||||
const killGather = () => {
|
||||
this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content');
|
||||
this.currentTask.kill(this);
|
||||
@@ -1091,10 +1168,11 @@ class CallSession extends Emitter {
|
||||
if (-1 === gatherPos) {
|
||||
/* no gather in the stack simply append tasks */
|
||||
this.tasks.push(...newTasks);
|
||||
/*
|
||||
this.logger.debug({
|
||||
updatedTaskList: listTaskNames(this.tasks)
|
||||
}, 'CallSession:_injectTasks - completed (simple append)');
|
||||
|
||||
*/
|
||||
/* we do need to kill the current gather if we are executing one */
|
||||
if (currentlyExecutingGather) killGather();
|
||||
return;
|
||||
@@ -1122,12 +1200,10 @@ class CallSession extends Emitter {
|
||||
this.replaceApplication(t);
|
||||
}
|
||||
else if (process.env.JAMBONES_INJECT_CONTENT) {
|
||||
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks (injecting content)');
|
||||
this._injectTasks(t);
|
||||
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||
}
|
||||
else {
|
||||
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
|
||||
this.tasks.push(...t);
|
||||
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||
}
|
||||
@@ -1171,7 +1247,7 @@ class CallSession extends Emitter {
|
||||
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
|
||||
}
|
||||
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 = null;
|
||||
}
|
||||
@@ -1402,9 +1478,15 @@ class CallSession extends Emitter {
|
||||
async _onReinvite(req, res) {
|
||||
try {
|
||||
if (this.ep) {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
if (this.isSipRecCallSession) {
|
||||
this.logger.info('handling reINVITE for siprec call');
|
||||
res.send(200, {body: this.ep.local.sdp});
|
||||
}
|
||||
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) {
|
||||
this.logger.info('handling reINVITE after media has been released');
|
||||
@@ -1622,7 +1704,7 @@ class CallSession extends Emitter {
|
||||
* @param {number} sipStatus - current sip status
|
||||
* @param {number} [duration] - duration of a completed call, in seconds
|
||||
*/
|
||||
_notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
if (this.callMoved) return;
|
||||
|
||||
/* race condition: we hang up at the same time as the caller */
|
||||
@@ -1642,7 +1724,7 @@ class CallSession extends Emitter {
|
||||
try {
|
||||
const b3 = this.b3;
|
||||
const httpHeaders = b3 && {b3};
|
||||
this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON(), httpHeaders);
|
||||
await this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON(), httpHeaders);
|
||||
span.end();
|
||||
} catch (err) {
|
||||
span.end();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const InboundCallSession = require('./inbound-call-session');
|
||||
const {createSipRecPayload} = require('../utils/siprec-utils');
|
||||
const {CallStatus} = require('../utils/constants');
|
||||
const {parseSiprecPayload} = require('../utils/siprec-utils');
|
||||
/**
|
||||
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is
|
||||
* established for an inbound SIPREC call.
|
||||
@@ -16,6 +17,32 @@ class SipRecCallSession extends InboundCallSession {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
async _onReinvite(req, res) {
|
||||
try {
|
||||
this.logger.info(req.payload, 'SipRec Re-INVITE payload');
|
||||
const {sdp1: reSdp1, sdp2: reSdp2, metadata: reMetadata} = await parseSiprecPayload(req, this.logger);
|
||||
this.sdp1 = reSdp1;
|
||||
this.sdp2 = reSdp2;
|
||||
this.metadata = reMetadata;
|
||||
|
||||
if (this.ep && this.ep2) {
|
||||
let remoteSdp = this.sdp1.replace(/sendonly/, 'sendrecv');
|
||||
const newSdp1 = await this.ep.modify(remoteSdp);
|
||||
remoteSdp = this.sdp2.replace(/sendonly/, 'sendrecv');
|
||||
const newSdp2 = await this.ep2.modify(remoteSdp);
|
||||
const combinedSdp = await createSipRecPayload(newSdp1, newSdp2, this.logger);
|
||||
res.send(200, {body: combinedSdp});
|
||||
this.logger.info({offer: req.body, answer: combinedSdp}, 'SipRec handling reINVITE');
|
||||
}
|
||||
else {
|
||||
this.logger.info('got SipRec reINVITE but no endpoint and media has not been released');
|
||||
res.send(488);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
}
|
||||
|
||||
async answerSipRecCall() {
|
||||
try {
|
||||
this.ms = this.getMS();
|
||||
|
||||
@@ -2,7 +2,7 @@ const Task = require('./task');
|
||||
const Emitter = require('events');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const makeTask = require('./make_task');
|
||||
const bent = require('bent');
|
||||
const assert = require('assert');
|
||||
|
||||
@@ -8,7 +8,8 @@ class TaskConfig extends Task {
|
||||
'synthesizer',
|
||||
'recognizer',
|
||||
'bargeIn',
|
||||
'record'
|
||||
'record',
|
||||
'listen'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if ('notifyEvents' in this.data) {
|
||||
@@ -30,7 +31,7 @@ class TaskConfig extends Task {
|
||||
});
|
||||
}
|
||||
if (this.bargeIn.sticky) this.autoEnable = true;
|
||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.data.amd) ?
|
||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ?
|
||||
TaskPreconditions.Endpoint :
|
||||
TaskPreconditions.None;
|
||||
}
|
||||
@@ -38,8 +39,9 @@ class TaskConfig extends Task {
|
||||
get name() { return TaskName.Config; }
|
||||
|
||||
get hasSynthesizer() { return Object.keys(this.synthesizer).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() {
|
||||
const phrase = [];
|
||||
@@ -54,6 +56,10 @@ class TaskConfig extends Task {
|
||||
const s = `{${v},${l}}`;
|
||||
phrase.push(`set recognizer${s}`);
|
||||
}
|
||||
if (this.hasRecording) phrase.push(this.record.action);
|
||||
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(',')}`;
|
||||
@@ -64,8 +70,7 @@ class TaskConfig extends Task {
|
||||
|
||||
if (this.notifyEvents) {
|
||||
this.logger.debug(`turning event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||
cs.notifyEvents = !!this.data.notifEvents;
|
||||
|
||||
cs.notifyEvents = !!this.data.notifyEvents;
|
||||
}
|
||||
|
||||
if (this.data.amd) {
|
||||
@@ -147,11 +152,21 @@ class TaskConfig extends Task {
|
||||
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) {
|
||||
super.kill(cs);
|
||||
if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
|
||||
//if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
|
||||
}
|
||||
|
||||
_onAmdEvent(cs, evt) {
|
||||
|
||||
@@ -400,15 +400,19 @@ class TaskDial extends Task {
|
||||
let fqdn;
|
||||
|
||||
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 = {
|
||||
headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers,
|
||||
headers: this.headers,
|
||||
proxy: `sip:${sbcAddress}`,
|
||||
callingNumber: this.callerId || req.callingNumber
|
||||
};
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
'X-Account-Sid': cs.accountSid
|
||||
};
|
||||
|
||||
const t = this.target.find((t) => t.type === 'teams');
|
||||
if (t) {
|
||||
|
||||
@@ -3,7 +3,7 @@ const {TaskName, TaskPreconditions} = require('../../utils/constants');
|
||||
const Intent = require('./intent');
|
||||
const DigitBuffer = require('./digit-buffer');
|
||||
const Transcription = require('./transcription');
|
||||
const normalizeJambones = require('../../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
class Dialogflow extends Task {
|
||||
constructor(logger, opts) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Task = require('./task');
|
||||
const Emitter = require('events');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const makeTask = require('./make_task');
|
||||
const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants');
|
||||
const bent = require('bent');
|
||||
|
||||
@@ -7,7 +7,10 @@ const {
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
DeepgramTranscriptionEvents,
|
||||
IbmTranscriptionEvents
|
||||
SonioxTranscriptionEvents,
|
||||
IbmTranscriptionEvents,
|
||||
NvidiaTranscriptionEvents,
|
||||
JambonzTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
|
||||
const makeTask = require('./make_task');
|
||||
@@ -32,14 +35,16 @@ class TaskGather extends Task {
|
||||
setChannelVarsForStt,
|
||||
normalizeTranscription,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime
|
||||
setSpeechCredentialsAtRuntime,
|
||||
compileSonioxTranscripts
|
||||
} = require('../utils/transcription-utils')(logger);
|
||||
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||
this.normalizeTranscription = normalizeTranscription;
|
||||
this.removeSpeechListeners = removeSpeechListeners;
|
||||
this.compileSonioxTranscripts = compileSonioxTranscripts;
|
||||
|
||||
[
|
||||
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
'speechTimeout', 'timeout', 'say', 'play'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
@@ -49,9 +54,9 @@ class TaskGather extends Task {
|
||||
|
||||
/* timeout of zero means no timeout */
|
||||
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
|
||||
this.interim = !!this.partialResultHook || this.bargein;
|
||||
this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0);
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 0;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
@@ -65,6 +70,11 @@ class TaskGather extends Task {
|
||||
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
|
||||
this.isContinuousAsr = this.asrTimeout > 0;
|
||||
|
||||
if (Array.isArray(this.data.recognizer.hints) &&
|
||||
0 == this.data.recognizer.hints.length && process.env.JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS) {
|
||||
logger.debug('Gather: an empty hints array was supplied, so we will mask global hints');
|
||||
this.maskGlobalSttHints = true;
|
||||
}
|
||||
this.data.recognizer.hints = this.data.recognizer.hints || [];
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
|
||||
}
|
||||
@@ -84,13 +94,21 @@ class TaskGather extends Task {
|
||||
/* buffer speech for continuous asr */
|
||||
this._bufferedTranscripts = [];
|
||||
|
||||
/* buffer for soniox transcripts */
|
||||
this._sonioxTranscripts = [];
|
||||
|
||||
this.parentTask = parentTask;
|
||||
this.partialTranscriptsCount = 0;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Gather; }
|
||||
|
||||
get needsStt() { return this.input.includes('speech'); }
|
||||
|
||||
get wantsSingleUtterance() {
|
||||
return this.data.recognizer?.singleUtterance === true;
|
||||
}
|
||||
|
||||
get earlyMedia() {
|
||||
return (this.sayTask && this.sayTask.earlyMedia) ||
|
||||
(this.playTask && this.playTask.earlyMedia);
|
||||
@@ -112,14 +130,17 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
this.logger.debug('Gather:exec');
|
||||
this.logger.debug({options: this.data}, 'Gather:exec');
|
||||
await super.exec(cs);
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
|
||||
|
||||
if (cs.hasGlobalSttHints) {
|
||||
if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) {
|
||||
const {hints, hintsBoost} = cs.globalSttHints;
|
||||
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints);
|
||||
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');
|
||||
@@ -141,9 +162,10 @@ class TaskGather extends Task {
|
||||
asrDtmfTerminationDigit: this.asrDtmfTerminationDigit
|
||||
}, 'Gather:exec - enabling continuous ASR since it is turned on for the session');
|
||||
}
|
||||
if (process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH &&
|
||||
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.hints.length > 0 && this.hints.length <= 10) {
|
||||
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');
|
||||
@@ -171,12 +193,14 @@ class TaskGather extends Task {
|
||||
vendor: this.vendor
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
// Notify application that STT vender is wrong.
|
||||
this.notifyError(`No speech-to-text service credentials for ${this.vendor} have been configured`);
|
||||
this.notifyError({
|
||||
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`);
|
||||
}
|
||||
|
||||
this.logger.info({sttCredentials: this.sttCredentials}, 'Gather:exec - sttCredentials');
|
||||
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
|
||||
/* get nuance access token */
|
||||
const {client_id, secret} = this.sttCredentials;
|
||||
@@ -195,7 +219,6 @@ class TaskGather extends Task {
|
||||
this._startTimer();
|
||||
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
|
||||
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
||||
this.logger.debug('Gather:exec - calling _initSpeech');
|
||||
this._initSpeech(cs, ep)
|
||||
.then(() => {
|
||||
if (this.killed) {
|
||||
@@ -283,6 +306,7 @@ class TaskGather extends Task {
|
||||
this._killAudio(cs);
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
this._clearAsrTimer();
|
||||
this.playTask?.span.end();
|
||||
this.sayTask?.span.end();
|
||||
this._resolve('killed');
|
||||
@@ -296,6 +320,7 @@ class TaskGather extends Task {
|
||||
const {timeout} = opts;
|
||||
this.timeout = timeout;
|
||||
this._startTimer();
|
||||
return true;
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
@@ -335,7 +360,6 @@ class TaskGather extends Task {
|
||||
|
||||
async _initSpeech(cs, ep) {
|
||||
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
|
||||
this.logger.debug(opts, 'TaskGather:_initSpeech - channel vars');
|
||||
switch (this.vendor) {
|
||||
case 'google':
|
||||
this.bugname = 'google_transcribe';
|
||||
@@ -367,8 +391,6 @@ class TaskGather extends Task {
|
||||
this._onTranscriptionComplete.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.VadDetected,
|
||||
this._onVadDetected.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.Error,
|
||||
this._onNuanceError.bind(this, cs, ep));
|
||||
|
||||
/* stall timers until prompt finishes playing */
|
||||
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
|
||||
@@ -384,22 +406,54 @@ class TaskGather extends Task {
|
||||
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));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Error,
|
||||
this._onIbmError.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:
|
||||
this.notifyError(`Invalid vendor ${this.vendor}`);
|
||||
this.notifyTaskDone();
|
||||
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||
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 */
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep));
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
}
|
||||
@@ -499,7 +553,7 @@ class TaskGather extends Task {
|
||||
// make sure this is not a transcript from answering machine detection
|
||||
const bugname = fsEvent.getHeader('media-bugname');
|
||||
const finished = fsEvent.getHeader('transcription-session-finished');
|
||||
this.logger.debug({evt, bugname, finished}, 'Gather:_onTranscription');
|
||||
this.logger.debug({evt, bugname, finished}, `Gather:_onTranscription for vendor ${this.vendor}`);
|
||||
if (bugname && this.bugname !== bugname) return;
|
||||
|
||||
if (this.vendor === 'ibm') {
|
||||
@@ -507,12 +561,26 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language);
|
||||
if (evt.alternatives.length === 0) {
|
||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
||||
return;
|
||||
}
|
||||
|
||||
/* fast path: our first partial transcript exactly matches an early hint */
|
||||
if (this.earlyHintsMatch && evt.is_final === false && this.partialTranscriptsCount++ === 0) {
|
||||
const transcript = evt.alternatives[0].transcript?.toLowerCase();
|
||||
const hints = this.data.recognizer?.hints || [];
|
||||
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._bufferedTranscripts.reduce((count, e) => {
|
||||
return count + e.alternatives[0]?.transcript.split(' ').length;
|
||||
}, 0);
|
||||
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) {
|
||||
@@ -521,7 +589,6 @@ class TaskGather extends Task {
|
||||
}
|
||||
else {
|
||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
||||
//this._startTranscribing(ep);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -545,7 +612,9 @@ class TaskGather extends Task {
|
||||
return this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
|
||||
}
|
||||
this._startAsrTimer();
|
||||
return this._startTranscribing(ep);
|
||||
|
||||
/* 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 {
|
||||
if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) {
|
||||
@@ -556,6 +625,12 @@ class TaskGather extends Task {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -566,6 +641,8 @@ class TaskGather extends Task {
|
||||
others do not.
|
||||
*/
|
||||
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
|
||||
this._clearTimer();
|
||||
this._startTimer();
|
||||
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) {
|
||||
if (!this.playComplete) {
|
||||
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
|
||||
@@ -579,6 +656,13 @@ class TaskGather extends Task {
|
||||
this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt},
|
||||
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) {
|
||||
@@ -587,31 +671,53 @@ class TaskGather extends Task {
|
||||
this._killAudio(cs);
|
||||
}
|
||||
|
||||
if (!this.resolved && !this.killed && !this._bufferedTranscripts.length) {
|
||||
/**
|
||||
* By default, Gather asks google for multiple utterances.
|
||||
* The reason is that we can sometimes get an 'end_of_utterance' event without
|
||||
* getting a transcription. This can happen if someone coughs or mumbles.
|
||||
* For that reason don't ask for a single utterance and we'll terminate the transcribe operation
|
||||
* once we get a final transcript.
|
||||
* However, if the usr has specified a singleUtterance, then we need to restart here
|
||||
* since we dont have a final transcript yet.
|
||||
*/
|
||||
if (!this.resolved && !this.killed && !this._bufferedTranscripts.length && this.wantsSingleUtterance) {
|
||||
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');
|
||||
}
|
||||
_onNuanceError(cs, ep, evt) {
|
||||
const {code, error, details} = evt;
|
||||
if (code === 404 && error === 'No speech') {
|
||||
this.logger.debug({code, error, details}, 'TaskGather:_onNuanceError');
|
||||
return this._resolve('timeout');
|
||||
}
|
||||
this.logger.info({code, error, details}, 'TaskGather:_onNuanceError');
|
||||
if (code === 413 && error === 'Too much speech') {
|
||||
return this._resolve('timeout');
|
||||
}
|
||||
}
|
||||
_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;
|
||||
@@ -623,7 +729,20 @@ class TaskGather extends Task {
|
||||
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.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();
|
||||
}
|
||||
|
||||
@@ -641,7 +760,7 @@ class TaskGather extends Task {
|
||||
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.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor IBM: ${reason}`});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -675,6 +794,10 @@ class TaskGather extends Task {
|
||||
if (this.resolved) return;
|
||||
|
||||
this.resolved = true;
|
||||
// Clear dtmf event
|
||||
if (this.dtmfBargein) {
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
}
|
||||
clearTimeout(this.interDigitTimer);
|
||||
this._clearTimer();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
class Lex extends Task {
|
||||
constructor(logger, opts) {
|
||||
|
||||
@@ -2,6 +2,7 @@ const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const moment = require('moment');
|
||||
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
|
||||
|
||||
class TaskListen extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
@@ -10,7 +11,7 @@ class TaskListen extends Task {
|
||||
|
||||
[
|
||||
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
|
||||
'sampleRate', 'timeout', 'transcribe', 'wsAuth'
|
||||
'sampleRate', 'timeout', 'transcribe', 'wsAuth', 'disableBidirectionalAudio'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
|
||||
this.mixType = this.mixType || 'mono';
|
||||
@@ -20,6 +21,8 @@ class TaskListen extends Task {
|
||||
this.nested = parentTask instanceof Task;
|
||||
|
||||
this.results = {};
|
||||
this.playAudioQueue = [];
|
||||
this.isPlayingAudioFromQueue = false;
|
||||
|
||||
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
|
||||
}
|
||||
@@ -58,6 +61,7 @@ class TaskListen extends Task {
|
||||
super.kill(cs);
|
||||
this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`);
|
||||
this._clearTimer();
|
||||
this.playAudioQueue = [];
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.logger.debug('TaskListen:kill closing websocket');
|
||||
try {
|
||||
@@ -136,7 +140,9 @@ class TaskListen extends Task {
|
||||
}
|
||||
|
||||
/* support bi-directional audio */
|
||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||
if (!this.disableBiDirectionalAudio) {
|
||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||
}
|
||||
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
|
||||
}
|
||||
@@ -182,16 +188,36 @@ class TaskListen extends Task {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onPlayAudio(ep, evt) {
|
||||
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
|
||||
async _playAudio(ep, evt, logger) {
|
||||
try {
|
||||
const results = await ep.play(evt.file);
|
||||
this.logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
||||
logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
||||
ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error playing file');
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.error({err}, 'Error playing file');
|
||||
}
|
||||
|
||||
async _onPlayAudio(ep, evt) {
|
||||
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
|
||||
if (!evt.queuePlay) {
|
||||
this.playAudioQueue = [];
|
||||
this._playAudio(ep, evt, this.logger);
|
||||
this.isPlayingAudioFromQueue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.playAudioQueue.length <= MAX_PLAY_AUDIO_QUEUE_SIZE) {
|
||||
this.playAudioQueue.push(evt);
|
||||
}
|
||||
|
||||
if (this.isPlayingAudioFromQueue) return;
|
||||
|
||||
this.isPlayingAudioFromQueue = true;
|
||||
while (this.playAudioQueue.length > 0) {
|
||||
await this._playAudio(ep, this.playAudioQueue.shift(), this.logger);
|
||||
}
|
||||
this.isPlayingAudioFromQueue = false;
|
||||
}
|
||||
|
||||
_onKillAudio(ep) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Task = require('./task');
|
||||
const { validateVerb } = require('@jambonz/verb-specifications');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const errBadInstruction = new Error('malformed jambonz application payload');
|
||||
|
||||
@@ -12,7 +12,7 @@ function makeTask(logger, obj, parent) {
|
||||
if (typeof data !== 'object') {
|
||||
throw errBadInstruction;
|
||||
}
|
||||
Task.validate(name, data);
|
||||
validateVerb(name, data, logger);
|
||||
switch (name) {
|
||||
case TaskName.SipDecline:
|
||||
const TaskSipDecline = require('./sip_decline');
|
||||
|
||||
@@ -37,6 +37,7 @@ class TaskPlay extends Task {
|
||||
}, this.timeoutSecs * 1000);
|
||||
}
|
||||
try {
|
||||
this.notifyStatus({event: 'start-playback'});
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
@@ -80,7 +81,8 @@ class TaskPlay extends Task {
|
||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||
}
|
||||
else {
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
this.notifyStatus({event: 'kill-playback'});
|
||||
this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
/**
|
||||
* Manages an outdial made via REST API
|
||||
@@ -27,7 +27,7 @@ class TaskRestDial extends Task {
|
||||
*/
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
this.req = cs.req;
|
||||
this.canCancel = true;
|
||||
|
||||
this._setCallTimer();
|
||||
await this.awaitTaskDone();
|
||||
@@ -36,15 +36,15 @@ class TaskRestDial extends Task {
|
||||
kill(cs) {
|
||||
super.kill(cs);
|
||||
this._clearCallTimer();
|
||||
if (this.req) {
|
||||
this.req.cancel();
|
||||
this.req = null;
|
||||
if (this.canCancel && cs?.req) {
|
||||
this.canCancel = false;
|
||||
cs.req.cancel();
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onConnect(dlg) {
|
||||
this.req = null;
|
||||
this.canCancel = false;
|
||||
const cs = this.callSession;
|
||||
cs.setDialog(dlg);
|
||||
|
||||
@@ -79,7 +79,7 @@ class TaskRestDial extends Task {
|
||||
_onCallStatus(status) {
|
||||
this.logger.debug(`CallStatus: ${status}`);
|
||||
if (status >= 200) {
|
||||
this.req = null;
|
||||
this.canCancel = false;
|
||||
this._clearCallTimer();
|
||||
if (status !== 200) this.notifyTaskDone();
|
||||
}
|
||||
|
||||
105
lib/tasks/say.js
105
lib/tasks/say.js
@@ -1,96 +1,26 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const pollySSMLSplit = require('polly-ssml-split');
|
||||
|
||||
const breakLengthyTextIfNeeded = (logger, text) => {
|
||||
const chunkSize = 1000;
|
||||
if (text.length <= chunkSize) return [text];
|
||||
|
||||
const result = [];
|
||||
const isSSML = text.startsWith('<speak>');
|
||||
let startPos = 0;
|
||||
let charPos = isSSML ? 7 : 0; // skip <speak>
|
||||
let tag;
|
||||
//logger.debug({isSSML}, `breakLengthyTextIfNeeded: handling text of length ${text.length}`);
|
||||
while (startPos + charPos < text.length) {
|
||||
if (isSSML && !tag && text[startPos + charPos] === '<') {
|
||||
const tagStartPos = ++charPos;
|
||||
while (startPos + charPos < text.length) {
|
||||
if (text[startPos + charPos] === '>') {
|
||||
if (text[startPos + charPos - 1] === '\\') tag = null;
|
||||
else if (!tag) tag = text.substring(startPos + tagStartPos, startPos + charPos - 1);
|
||||
break;
|
||||
}
|
||||
if (!tag) {
|
||||
const c = text[startPos + charPos];
|
||||
if (c === ' ') {
|
||||
tag = text.substring(startPos + tagStartPos, startPos + charPos);
|
||||
//logger.debug(`breakLengthyTextIfNeeded: enter tag ${tag} (space)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
charPos++;
|
||||
}
|
||||
if (tag) {
|
||||
//search for end of tag
|
||||
//logger.debug(`breakLengthyTextIfNeeded: searching forward for </${tag}>`);
|
||||
const e1 = text.indexOf(`</${tag}>`, startPos + charPos);
|
||||
const e2 = text.indexOf('/>', startPos + charPos);
|
||||
const tagEndPos = e1 === -1 ? e2 : e2 === -1 ? e1 : Math.min(e1, e2);
|
||||
if (tagEndPos === -1) {
|
||||
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} not found, exiting`);
|
||||
} else {
|
||||
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} found at ${tagEndPos}`);
|
||||
charPos = tagEndPos + 1;
|
||||
}
|
||||
tag = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (charPos < chunkSize) {
|
||||
charPos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// start looking for a good break point
|
||||
let chunkIt = false;
|
||||
const a = text[startPos + charPos];
|
||||
const b = text[startPos + charPos + 1];
|
||||
if (/[\.!\?]/.test(a) && /\s/.test(b)) {
|
||||
//logger.debug('breakLengthyTextIfNeeded: breaking at sentence end');
|
||||
chunkIt = true;
|
||||
}
|
||||
if (chunkIt) {
|
||||
charPos++;
|
||||
const chunk = text.substr(startPos, charPos);
|
||||
if (isSSML) {
|
||||
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}</speak>`);
|
||||
}
|
||||
else result.push(chunk);
|
||||
charPos = 0;
|
||||
startPos += chunk.length;
|
||||
|
||||
//logger.debug({chunk: result[result.length - 1]},
|
||||
// `breakLengthyTextIfNeeded: chunked; new starting pos ${startPos}`);
|
||||
|
||||
}
|
||||
else charPos++;
|
||||
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];
|
||||
}
|
||||
|
||||
// final chunk
|
||||
if (startPos < text.length) {
|
||||
const chunk = text.substr(startPos);
|
||||
if (isSSML) {
|
||||
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}`);
|
||||
}
|
||||
else result.push(chunk);
|
||||
|
||||
//logger.debug({chunk: result[result.length - 1]},
|
||||
// `breakLengthyTextIfNeeded: final chunk; starting pos ${startPos} length ${chunk.length}`);
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
class TaskSay extends Task {
|
||||
@@ -213,7 +143,7 @@ class TaskSay extends Task {
|
||||
span.end();
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
||||
alert_type: AlertType.TTS_FAILURE,
|
||||
vendor,
|
||||
detail: err.message
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
@@ -224,7 +154,6 @@ class TaskSay extends Task {
|
||||
|
||||
const arr = this.text.map((t) => generateAudio(t));
|
||||
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
||||
this.logger.debug({filepath}, 'synthesized files for tts');
|
||||
this.notifyStatus({event: 'start-playback'});
|
||||
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
|
||||
|
||||
@@ -1,759 +0,0 @@
|
||||
{
|
||||
"sip:decline": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"status": "number",
|
||||
"reason": "string",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"sip:request": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"method": "string",
|
||||
"body": "string",
|
||||
"headers": "object",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
]
|
||||
},
|
||||
"sip:refer": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"referTo": "string",
|
||||
"referredBy": "string",
|
||||
"headers": "object",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"referTo"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"synthesizer": "#synthesizer",
|
||||
"recognizer": "#recognizer",
|
||||
"bargeIn": "#bargeIn",
|
||||
"record": "#recordOptions",
|
||||
"amd": "#amd",
|
||||
"notifyEvents": "boolean"
|
||||
},
|
||||
"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": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"actionHook": "object|string",
|
||||
"timeout": "number",
|
||||
"beep": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"enqueue": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"actionHook": "object|string",
|
||||
"waitHook": "object|string",
|
||||
"_": "object"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"leave": {
|
||||
"properties": {
|
||||
"id": "string"
|
||||
}
|
||||
},
|
||||
"hangup": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"play": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"url": "string|array",
|
||||
"loop": "number|string",
|
||||
"earlyMedia": "boolean",
|
||||
"seekOffset": "number|string",
|
||||
"timeoutSecs": "number|string",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"say": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"text": "string|array",
|
||||
"loop": "number|string",
|
||||
"synthesizer": "#synthesizer",
|
||||
"earlyMedia": "boolean",
|
||||
"disableTtsCache": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"gather": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"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": {
|
||||
"id": "string",
|
||||
"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": {
|
||||
"id": "string",
|
||||
"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",
|
||||
"amd": "#amd"
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
]
|
||||
},
|
||||
"dialogflow": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"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": {
|
||||
"id": "string",
|
||||
"dtmf": "string",
|
||||
"duration": "number"
|
||||
},
|
||||
"required": [
|
||||
"dtmf"
|
||||
]
|
||||
},
|
||||
"lex": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"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": {
|
||||
"id": "string",
|
||||
"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": {
|
||||
"id": "string",
|
||||
"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": {
|
||||
"id": "string",
|
||||
"length": "number"
|
||||
},
|
||||
"required": [
|
||||
"length"
|
||||
]
|
||||
},
|
||||
"rasa": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"url": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"tts": "#synthesizer",
|
||||
"prompt": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"record": {
|
||||
"properties": {
|
||||
"path": "string"
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"recordOptions": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"]
|
||||
},
|
||||
"recordingID": "string",
|
||||
"siprecServerURL": "string"
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"rest:dial": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"account_sid": "string",
|
||||
"application_sid": "string",
|
||||
"call_hook": "object|string",
|
||||
"call_status_hook": "object|string",
|
||||
"from": "string",
|
||||
"fromHost": "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": {
|
||||
"id": "string",
|
||||
"data": "object"
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"transcribe": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"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",
|
||||
"from": "#dialFrom",
|
||||
"name": "string",
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
"auth": "#auth",
|
||||
"vmail": "boolean",
|
||||
"tenant": "string",
|
||||
"trunk": "string",
|
||||
"overrideTo": "string"
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"dialFrom": {
|
||||
"properties": {
|
||||
"user": "string",
|
||||
"host": "string"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"properties": {
|
||||
"username": "string",
|
||||
"password": "string"
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password"
|
||||
]
|
||||
},
|
||||
"synthesizer": {
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "polly", "microsoft", "nuance", "ibm", "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", "nuance", "deepgram", "ibm", "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",
|
||||
"azureSttEndpointId": "string",
|
||||
"asrDtmfTerminationDigit": "string",
|
||||
"asrTimeout": "number",
|
||||
"nuanceOptions": "#nuanceOptions",
|
||||
"deepgramOptions": "#deepgramOptions",
|
||||
"ibmOptions": "#ibmOptions"
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
]
|
||||
},
|
||||
"ibmOptions": {
|
||||
"properties": {
|
||||
"sttApiKey": "string",
|
||||
"sttRegion": "string",
|
||||
"ttsApiKey": "string",
|
||||
"ttsRegion": "string",
|
||||
"instanceId": "string",
|
||||
"model": "string",
|
||||
"languageCustomizationId": "string",
|
||||
"acousticCustomizationId": "string",
|
||||
"baseModelVersion": "string",
|
||||
"watsonMetadata": "string",
|
||||
"watsonLearningOptOut": "boolean"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"deepgramOptions": {
|
||||
"properties": {
|
||||
"apiKey": "string",
|
||||
"tier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enhanced",
|
||||
"base"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"general",
|
||||
"meeting",
|
||||
"phonecall",
|
||||
"voicemail",
|
||||
"finance",
|
||||
"conversationalai",
|
||||
"video",
|
||||
"custom"
|
||||
]
|
||||
},
|
||||
"customModel": "string",
|
||||
"version": "string",
|
||||
"punctuate": "boolean",
|
||||
"profanityFilter": "boolean",
|
||||
"redact": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pci",
|
||||
"numbers",
|
||||
"true",
|
||||
"ssn"
|
||||
]
|
||||
},
|
||||
"diarize": "boolean",
|
||||
"diarizeVersion": "string",
|
||||
"ner": "boolean",
|
||||
"multichannel": "boolean",
|
||||
"alternatives": "number",
|
||||
"numerals": "boolean",
|
||||
"search": "array",
|
||||
"replace": "array",
|
||||
"keywords": "array",
|
||||
"endpointing": "boolean",
|
||||
"vadTurnoff": "number",
|
||||
"tag": "string"
|
||||
}
|
||||
},
|
||||
"nuanceOptions": {
|
||||
"properties": {
|
||||
"clientId": "string",
|
||||
"secret": "string",
|
||||
"kryptonEndpoint": "string",
|
||||
"topic": "string",
|
||||
"utteranceDetectionMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"single",
|
||||
"multiple",
|
||||
"disabled"
|
||||
]
|
||||
},
|
||||
"punctuation": "boolean",
|
||||
"profanityFilter": "boolean",
|
||||
"includeTokenization": "boolean",
|
||||
"discardSpeakerAdaptation": "boolean",
|
||||
"suppressCallRecording": "boolean",
|
||||
"maskLoadFailures": "boolean",
|
||||
"suppressInitialCapitalization": "boolean",
|
||||
"allowZeroBaseLmWeight": "boolean",
|
||||
"filterWakeupWord": "boolean",
|
||||
"resultType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"final",
|
||||
"partial",
|
||||
"immutable_partial"
|
||||
]
|
||||
},
|
||||
"noInputTimeoutMs": "number",
|
||||
"recognitionTimeoutMs": "number",
|
||||
"utteranceEndSilenceMs": "number",
|
||||
"maxHypotheses": "number",
|
||||
"speechDomain": "string",
|
||||
"formatting": "#formatting",
|
||||
"clientData": "object",
|
||||
"userId": "string",
|
||||
"speechDetectionSensitivity": "number",
|
||||
"resources": ["#resource"]
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"resource": {
|
||||
"properties": {
|
||||
"externalReference": "#resourceReference",
|
||||
"inlineWordset": "string",
|
||||
"builtin": "string",
|
||||
"inlineGrammar": "string",
|
||||
"wakeupWord": "[string]",
|
||||
"weightName": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"defaultWeight",
|
||||
"lowest",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"highest"
|
||||
]
|
||||
},
|
||||
"weightValue": "number",
|
||||
"reuse": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"undefined_reuse",
|
||||
"low_reuse",
|
||||
"high_reuse"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"resourceReference": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"undefined_resource_type",
|
||||
"wordset",
|
||||
"compiled_wordset",
|
||||
"domain_lm",
|
||||
"speaker_profile",
|
||||
"grammar",
|
||||
"settings"
|
||||
]
|
||||
},
|
||||
"uri": "string",
|
||||
"maxLoadFailures": "boolean",
|
||||
"requestTimeoutMs": "number",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"formatting": {
|
||||
"properties": {
|
||||
"scheme": "string",
|
||||
"options": "object"
|
||||
},
|
||||
"required": [
|
||||
"scheme",
|
||||
"options"
|
||||
]
|
||||
},
|
||||
"lexIntent": {
|
||||
"properties": {
|
||||
"name": "string",
|
||||
"slots": "object"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"vad": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"voiceMs": "number",
|
||||
"mode": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
"amd": {
|
||||
"properties": {
|
||||
"actionHook": "object|string",
|
||||
"thresholdWordCount": "number",
|
||||
"timers": "#amdTimers",
|
||||
"recognizer": "#recognizer"
|
||||
},
|
||||
"required": [
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"amdTimers": {
|
||||
"properties": {
|
||||
"noSpeechTimeoutMs": "number",
|
||||
"decisionTimeoutMs": "number",
|
||||
"toneTimeoutMs": "number",
|
||||
"greetingCompletionTimeoutMs": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
const Emitter = require('events');
|
||||
const uuidv4 = require('uuid-random');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const assert = require('assert');
|
||||
const {TaskPreconditions} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
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
|
||||
@@ -287,77 +282,6 @@ class Task extends Emitter {
|
||||
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 if (dKey === '_') {
|
||||
/* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */
|
||||
}
|
||||
else throw new Error(`${name}: unknown property ${dKey}`);
|
||||
}
|
||||
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Task;
|
||||
|
||||
@@ -3,13 +3,16 @@ const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
NuanceTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
DeepgramTranscriptionEvents,
|
||||
IbmTranscriptionEvents
|
||||
SonioxTranscriptionEvents,
|
||||
IbmTranscriptionEvents,
|
||||
NvidiaTranscriptionEvents,
|
||||
JambonzTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
class TaskTranscribe extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
@@ -21,11 +24,13 @@ class TaskTranscribe extends Task {
|
||||
setChannelVarsForStt,
|
||||
normalizeTranscription,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime
|
||||
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.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||
@@ -39,6 +44,9 @@ class TaskTranscribe extends Task {
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
|
||||
/* buffer for soniox transcripts */
|
||||
this._sonioxTranscripts = [];
|
||||
|
||||
recognizer.hints = recognizer.hints || [];
|
||||
recognizer.altLanguages = recognizer.altLanguages || [];
|
||||
}
|
||||
@@ -182,8 +190,6 @@ class TaskTranscribe extends Task {
|
||||
this._onStartOfSpeech.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
|
||||
this._onTranscriptionComplete.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Error,
|
||||
this._onNuanceError.bind(this, cs, ep, channel));
|
||||
break;
|
||||
case 'deepgram':
|
||||
this.bugname = 'deepgram_transcribe';
|
||||
@@ -194,7 +200,11 @@ class TaskTranscribe extends Task {
|
||||
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,
|
||||
@@ -203,14 +213,25 @@ class TaskTranscribe extends Task {
|
||||
this._onIbmConnect.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
|
||||
this._onIbmConnectFailure.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Error,
|
||||
this._onIbmError.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(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep));
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
|
||||
@@ -238,8 +259,11 @@ class TaskTranscribe extends Task {
|
||||
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization');
|
||||
|
||||
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language);
|
||||
|
||||
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription');
|
||||
if (evt.alternatives.length === 0) {
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening');
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.alternatives[0]?.transcript === '' && !cs.callGone && !this.killed) {
|
||||
if (['microsoft', 'deepgram'].includes(this.vendor)) {
|
||||
@@ -252,6 +276,15 @@ class TaskTranscribe extends Task {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.vendor === 'soniox') {
|
||||
/* compile transcripts into one */
|
||||
this._sonioxTranscripts.push(evt.vendor.finalWords);
|
||||
if (evt.is_final) {
|
||||
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
|
||||
this._sonioxTranscripts = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.transcriptionHook) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
@@ -300,17 +333,6 @@ class TaskTranscribe extends Task {
|
||||
this._timer = null;
|
||||
}
|
||||
}
|
||||
_onNuanceError(_cs, _ep, _channel, evt) {
|
||||
const {code, error, details} = evt;
|
||||
if (code === 404 && error === 'No speech') {
|
||||
this.logger.debug({code, error, details}, 'TaskTranscribe:_onNuanceError');
|
||||
return this._resolve('timeout');
|
||||
}
|
||||
this.logger.info({code, error, details}, 'TaskTranscribe:_onNuanceError');
|
||||
if (code === 413 && error === 'Too much speech') {
|
||||
return this._resolve('timeout');
|
||||
}
|
||||
}
|
||||
_onDeepgramConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskTranscribe:_onDeepgramConnect');
|
||||
}
|
||||
@@ -347,7 +369,25 @@ class TaskTranscribe extends Task {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
_onIbmError(cs, _ep, _channel, evt) {
|
||||
this.logger.info({evt}, 'TaskGather:_onIbmError');
|
||||
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}`});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -267,7 +267,6 @@ module.exports = (logger) => {
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
|
||||
}
|
||||
logger.debug({sttOpts}, 'startAmd: setting channel vars');
|
||||
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
|
||||
|
||||
amd
|
||||
|
||||
@@ -74,11 +74,22 @@
|
||||
"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",
|
||||
@@ -99,6 +110,12 @@
|
||||
"NoSpeechDetected": "azure_transcribe::no_speech_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": {
|
||||
"Connect": "mod_audio_fork::connect",
|
||||
"ConnectFailure": "mod_audio_fork::connect_failed",
|
||||
@@ -140,6 +157,7 @@
|
||||
"queue:status",
|
||||
"dial:confirm",
|
||||
"verb:hook",
|
||||
"verb:status",
|
||||
"jambonz:error"
|
||||
],
|
||||
"RecordState": {
|
||||
|
||||
@@ -50,6 +50,8 @@ const speechMapper = (cred) => {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_id = o.client_id;
|
||||
obj.secret = o.secret;
|
||||
obj.nuance_tts_uri = o.nuance_tts_uri;
|
||||
obj.nuance_stt_uri = o.nuance_stt_uri;
|
||||
}
|
||||
else if ('ibm' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -62,6 +64,16 @@ const speechMapper = (cred) => {
|
||||
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);
|
||||
}
|
||||
@@ -79,47 +91,13 @@ module.exports = (logger, srf) => {
|
||||
const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
|
||||
const speech = r2.map(speechMapper);
|
||||
|
||||
/* search at the service provider level if we don't find it at the account level */
|
||||
const haveGoogle = speech.find((s) => s.vendor === 'google');
|
||||
const haveAws = speech.find((s) => s.vendor === 'aws');
|
||||
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
|
||||
const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
|
||||
const haveNuance = speech.find((s) => s.vendor === 'nuance');
|
||||
const haveDeepgram = speech.find((s) => s.vendor === 'deepgram');
|
||||
const haveIbm = speech.find((s) => s.vendor === 'ibm');
|
||||
if (!haveGoogle || !haveAws || !haveMicrosoft || !haveWellsaid || !haveNuance || !haveIbm || !haveDeepgram) {
|
||||
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));
|
||||
}
|
||||
if (!haveNuance) {
|
||||
const nuance = r3.find((s) => s.vendor === 'nuance');
|
||||
if (nuance) speech.push(speechMapper(nuance));
|
||||
}
|
||||
if (!haveDeepgram) {
|
||||
const deepgram = r3.find((s) => s.vendor === 'deepgram');
|
||||
if (deepgram) speech.push(speechMapper(deepgram));
|
||||
}
|
||||
if (!haveIbm) {
|
||||
const ibm = r3.find((s) => s.vendor === 'ibm');
|
||||
if (ibm) speech.push(speechMapper(ibm));
|
||||
}
|
||||
/* add service provider creds unless we have that vendor at the account level */
|
||||
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
|
||||
r3.forEach((s) => {
|
||||
if (!speech.find((s2) => s2.vendor === s.vendor)) {
|
||||
speech.push(speechMapper(s));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...r[0],
|
||||
|
||||
@@ -2,9 +2,9 @@ const crypto = require('crypto');
|
||||
const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
|
||||
const iv = crypto.randomBytes(16);
|
||||
const secretKey = crypto.createHash('sha256')
|
||||
.update(String(process.env.JWT_SECRET))
|
||||
.update(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET)
|
||||
.digest('base64')
|
||||
.substr(0, 32);
|
||||
.substring(0, 32);
|
||||
|
||||
const encrypt = (text) => {
|
||||
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
|
||||
@@ -25,8 +25,8 @@ const decrypt = (data) => {
|
||||
throw err;
|
||||
}
|
||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrpyted.toString();
|
||||
const decrypted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrypted.toString();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -2,7 +2,6 @@ const {Client, Pool} = require('undici');
|
||||
const parseUrl = require('parse-url');
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const WsRequestor = require('./ws-requestor');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const pools = new Map();
|
||||
@@ -94,6 +93,7 @@ class HttpRequestor extends BaseRequestor {
|
||||
|
||||
/* if we have an absolute url, and it is ws then do a websocket connection */
|
||||
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);
|
||||
|
||||
@@ -138,7 +138,6 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveCall,
|
||||
listCalls,
|
||||
deleteCall,
|
||||
synthAudio,
|
||||
createHash,
|
||||
retrieveHash,
|
||||
deleteKey,
|
||||
@@ -151,11 +150,17 @@ function installSrfLocals(srf, logger) {
|
||||
pushBack,
|
||||
popFront,
|
||||
removeFromList,
|
||||
lengthOfList,
|
||||
getListPosition,
|
||||
lengthOfList,
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger, tracer);
|
||||
const {
|
||||
synthAudio,
|
||||
getNuanceAccessToken,
|
||||
getIbmAccessToken,
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
} = require('@jambonz/speech-utils')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger, tracer);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
|
||||
@@ -4,7 +4,7 @@ const SipError = require('drachtio-srf').SipError;
|
||||
const {TaskPreconditions, CallDirection} = require('../utils/constants');
|
||||
const CallInfo = require('../session/call-info');
|
||||
const assert = require('assert');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const AdultingCallSession = require('../session/adulting-call-session');
|
||||
|
||||
@@ -75,6 +75,10 @@ module.exports = (logger) => {
|
||||
}
|
||||
})();
|
||||
}
|
||||
else if (process.env.K8S) {
|
||||
lifecycleEmitter.scaleIn = () => process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
async function pingProxies(srf) {
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
|
||||
@@ -47,8 +47,16 @@ const parseSiprecPayload = (req, logger) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!sdp || !meta) {
|
||||
logger.info({payload: req.payload}, 'invalid SIPREC payload');
|
||||
|
||||
if (!meta && sdp) {
|
||||
const arr = /^([^]+)(m=[^]+?)(m=[^]+?)$/.exec(sdp);
|
||||
opts.sdp1 = `${arr[1]}${arr[2]}`;
|
||||
opts.sdp2 = `${arr[1]}${arr[3]}\r\n`;
|
||||
opts.sessionId = uuidv4();
|
||||
logger.info({ payload: req.payload }, 'SIPREC payload with no metadata (e.g. Cisco NBR)');
|
||||
resolve(opts);
|
||||
} else if (!sdp || !meta) {
|
||||
logger.info({ payload: req.payload }, 'invalid SIPREC payload');
|
||||
return reject(new Error('expected multipart SIPREC body'));
|
||||
}
|
||||
|
||||
@@ -242,7 +250,8 @@ const createSipRecPayload = (sdp1, sdp2, logger) => {
|
||||
.replace(/a=sendonly\r\n/g, '')
|
||||
.replace(/a=direction:both\r\n/g, '');
|
||||
*/
|
||||
return combinedSdp;
|
||||
|
||||
return combinedSdp.replace(/sendrecv/g, 'recvonly');
|
||||
};
|
||||
|
||||
module.exports = { parseSiprecPayload, createSipRecPayload } ;
|
||||
|
||||
@@ -5,8 +5,156 @@ const {
|
||||
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_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
|
||||
'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 || [])
|
||||
@@ -19,7 +167,7 @@ const normalizeDeepgram = (evt, channel, language) => {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives,
|
||||
alternatives: [alternatives[0]],
|
||||
vendor: {
|
||||
name: 'deepgram',
|
||||
evt: copy
|
||||
@@ -27,6 +175,25 @@ const normalizeDeepgram = (evt, channel, language) => {
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -50,7 +217,7 @@ const normalizeGoogle = (evt, channel, language) => {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives: evt.alternatives,
|
||||
alternatives: [evt.alternatives[0]],
|
||||
vendor: {
|
||||
name: 'google',
|
||||
evt: copy
|
||||
@@ -58,13 +225,22 @@ const normalizeGoogle = (evt, channel, language) => {
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
alternatives: [evt.alternatives[0]],
|
||||
vendor: {
|
||||
name: 'nuance',
|
||||
evt: copy
|
||||
@@ -92,7 +268,7 @@ const normalizeMicrosoft = (evt, channel, language) => {
|
||||
language_code,
|
||||
channel_tag: channel,
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives,
|
||||
alternatives: [alternatives[0]],
|
||||
vendor: {
|
||||
name: 'microsoft',
|
||||
evt: copy
|
||||
@@ -118,7 +294,7 @@ const normalizeAws = (evt, channel, language) => {
|
||||
module.exports = (logger) => {
|
||||
const normalizeTranscription = (evt, vendor, channel, language) => {
|
||||
|
||||
logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription');
|
||||
//logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription');
|
||||
switch (vendor) {
|
||||
case 'deepgram':
|
||||
return normalizeDeepgram(evt, channel, language);
|
||||
@@ -132,7 +308,14 @@ module.exports = (logger) => {
|
||||
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;
|
||||
}
|
||||
@@ -142,6 +325,7 @@ module.exports = (logger) => {
|
||||
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 = {
|
||||
@@ -151,59 +335,42 @@ module.exports = (logger) => {
|
||||
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
|
||||
};
|
||||
|
||||
if ('google' === rOpts.vendor) {
|
||||
if ('google' === vendor) {
|
||||
const model = task.name === TaskName.Gather ? 'command_and_search' : 'latest_long';
|
||||
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}),
|
||||
...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
|
||||
...(rOpts.separateRecognitionPerChannel && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
|
||||
...(rOpts.separateRecognitionPerChanne === false && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 0}),
|
||||
...(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 && {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.enhancedModel !== false && {GOOGLE_SPEECH_USE_ENHANCED: 1}),
|
||||
...(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.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}),
|
||||
...(typeof rOpts.hintsBoost === 'number' && {GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
|
||||
...(rOpts.altLanguages.length > 0 &&
|
||||
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: rOpts.altLanguages.join(',')}),
|
||||
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
|
||||
...(rOpts.interactionType &&
|
||||
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
|
||||
...{GOOGLE_SPEECH_MODEL: rOpts.model || (task.name === TaskName.Gather ? 'command_and_search' : 'phone_call')},
|
||||
...(rOpts.naicsCode > 0 &&
|
||||
{GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
|
||||
...{GOOGLE_SPEECH_MODEL: rOpts.model || model},
|
||||
...(rOpts.naicsCode > 0 && {GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
|
||||
GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE: 'phone_line',
|
||||
};
|
||||
}
|
||||
else if (['aws', 'polly'].includes(rOpts.vendor)) {
|
||||
else if (['aws', 'polly'].includes(vendor)) {
|
||||
opts = {
|
||||
...opts,
|
||||
...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
|
||||
@@ -216,7 +383,7 @@ module.exports = (logger) => {
|
||||
}),
|
||||
};
|
||||
}
|
||||
else if ('microsoft' === rOpts.vendor) {
|
||||
else if ('microsoft' === vendor) {
|
||||
opts = {
|
||||
...opts,
|
||||
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||
@@ -224,7 +391,7 @@ module.exports = (logger) => {
|
||||
...(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}),
|
||||
{AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
|
||||
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||
...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
|
||||
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
|
||||
@@ -241,7 +408,7 @@ module.exports = (logger) => {
|
||||
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint})
|
||||
};
|
||||
}
|
||||
else if ('nuance' === rOpts.vendor) {
|
||||
else if ('nuance' === vendor) {
|
||||
/**
|
||||
* Note: all nuance options are in recognizer.nuanceOptions, should migrate
|
||||
* other vendor settings to similar nested structure
|
||||
@@ -249,15 +416,12 @@ module.exports = (logger) => {
|
||||
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},
|
||||
...(sttCredentials.access_token) && {NUANCE_ACCESS_TOKEN: sttCredentials.access_token},
|
||||
...(sttCredentials.nuance_stt_uri) && {NUANCE_KRYPTON_ENDPOINT: sttCredentials.nuance_stt_uri},
|
||||
...(nuanceOptions.topic) && {NUANCE_TOPIC: nuanceOptions.topic},
|
||||
...(nuanceOptions.utteranceDetectionMode) &&
|
||||
{NUANCE_UTTERANCE_DETECTION_MODE: nuanceOptions.utteranceDetectionMode},
|
||||
...(nuanceOptions.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
|
||||
...(nuanceOptions.punctuation || rOpts.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
|
||||
...(nuanceOptions.profanityFilter) &&
|
||||
{NUANCE_FILTER_PROFANITY: nuanceOptions.profanityFilter},
|
||||
...(nuanceOptions.includeTokenization) &&
|
||||
@@ -292,7 +456,7 @@ module.exports = (logger) => {
|
||||
{NUANCE_RESOURCES: JSON.stringify(nuanceOptions.resources)},
|
||||
};
|
||||
}
|
||||
else if ('deepgram' === rOpts.vendor) {
|
||||
else if ('deepgram' === vendor) {
|
||||
const {deepgramOptions = {}} = rOpts;
|
||||
opts = {
|
||||
...opts,
|
||||
@@ -333,10 +497,33 @@ module.exports = (logger) => {
|
||||
...(deepgramOptions.vadTurnoff) &&
|
||||
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
|
||||
...(deepgramOptions.tag) &&
|
||||
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.tag}
|
||||
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
|
||||
};
|
||||
}
|
||||
else if ('ibm' === rOpts.vendor) {
|
||||
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,
|
||||
@@ -360,8 +547,58 @@ module.exports = (logger) => {
|
||||
{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})
|
||||
};
|
||||
|
||||
logger.debug({opts}, 'recognizer channel vars');
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -380,24 +617,45 @@ module.exports = (logger) => {
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete);
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech);
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.Error);
|
||||
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} = recognizer.nuanceOptions || {};
|
||||
const {clientId, secret, kryptonEndpoint} = recognizer.nuanceOptions || {};
|
||||
if (clientId && secret) return {client_id: clientId, secret};
|
||||
if (kryptonEndpoint) return {nuance_stt_uri: 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 {
|
||||
@@ -414,6 +672,7 @@ module.exports = (logger) => {
|
||||
normalizeTranscription,
|
||||
setChannelVarsForStt,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime
|
||||
setSpeechCredentialsAtRuntime,
|
||||
compileSonioxTranscripts
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ const short = require('short-uuid');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const Websocket = require('ws');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const HttpRequestor = require('./http-requestor');
|
||||
const MAX_RECONNECTS = 5;
|
||||
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
|
||||
|
||||
@@ -45,7 +44,7 @@ class WsRequestor extends BaseRequestor {
|
||||
return;
|
||||
}
|
||||
if (this.closedGracefully) {
|
||||
this.logger.debug(`WsRequestor:request - discarding ${type} because we closed the socket`);
|
||||
this.logger.debug(`WsRequestor:request - discarding ${type} because socket was closed gracefully`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,6 +52,7 @@ class WsRequestor extends BaseRequestor {
|
||||
|
||||
/* if we have an absolute url, and it is http then do a standard webhook */
|
||||
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
|
||||
const HttpRequestor = require('./http-requestor');
|
||||
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, h, this.secret);
|
||||
@@ -96,6 +96,9 @@ class WsRequestor extends BaseRequestor {
|
||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||
|
||||
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 obj = {
|
||||
type,
|
||||
@@ -118,8 +121,18 @@ class WsRequestor extends BaseRequestor {
|
||||
|
||||
//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 */
|
||||
if (['call:status', 'jambonz:error', 'session:reconnect'].includes(type)) {
|
||||
if (['call:status', 'verb:status', 'jambonz:error'].includes(type) || reconnectingWithoutAck) {
|
||||
this.ws.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
sendQueuedMsgs();
|
||||
@@ -131,7 +144,7 @@ class WsRequestor extends BaseRequestor {
|
||||
return new Promise((resolve, reject) => {
|
||||
/* give the far end a reasonable amount of time to ack our message */
|
||||
const timer = setTimeout(() => {
|
||||
const {failure} = this.messagesInFlight.get(msgid);
|
||||
const {failure} = this.messagesInFlight.get(msgid) || {};
|
||||
failure && failure(`timeout from far end for msgid ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
}, RESPONSE_TIMEOUT_MS);
|
||||
@@ -170,14 +183,7 @@ class WsRequestor extends BaseRequestor {
|
||||
this.ws.removeAllListeners();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
this._clearPendingMessages();
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'WsRequestor: Error closing socket');
|
||||
}
|
||||
@@ -213,7 +219,6 @@ class WsRequestor extends BaseRequestor {
|
||||
}
|
||||
|
||||
_setHandlers(ws) {
|
||||
this.logger.debug('WsRequestor:_setHandlers');
|
||||
ws
|
||||
.once('open', this._onOpen.bind(this, ws))
|
||||
.once('close', this._onClose.bind(this))
|
||||
@@ -222,6 +227,15 @@ class WsRequestor extends BaseRequestor {
|
||||
.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) {
|
||||
if (this.connections > 0) {
|
||||
this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
|
||||
@@ -259,12 +273,14 @@ class WsRequestor extends BaseRequestor {
|
||||
}, 'WsRequestor - unexpected response');
|
||||
this.emit('connection-failure');
|
||||
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
|
||||
this.connections++;
|
||||
}
|
||||
|
||||
_onSocketClosed() {
|
||||
this.ws = null;
|
||||
this.emit('connection-dropped');
|
||||
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`);
|
||||
setTimeout(() => {
|
||||
this.logger.debug(
|
||||
@@ -316,12 +332,13 @@ class WsRequestor extends BaseRequestor {
|
||||
}
|
||||
|
||||
_recvAck(msgid, data) {
|
||||
this._initMsgId = null;
|
||||
const obj = this.messagesInFlight.get(msgid);
|
||||
if (!obj) {
|
||||
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
|
||||
//this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
const {success} = obj;
|
||||
success && success(data);
|
||||
|
||||
4700
package-lock.json
generated
4700
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.8",
|
||||
"version": "v0.8.2",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -19,44 +19,49 @@
|
||||
"bugs": {},
|
||||
"scripts": {
|
||||
"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 HTTP_POOL=1 ENCRYPTION_SECRET=foobar 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/ ",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.7.3",
|
||||
"@jambonz/db-helpers": "^0.7.4",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.6.3",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/realtimedb-helpers": "^0.7.0",
|
||||
"@jambonz/speech-utils": "^0.0.12",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@opentelemetry/api": "^1.2.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.7.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.7.0",
|
||||
"@opentelemetry/instrumentation": "^0.27.0",
|
||||
"@opentelemetry/resources": "^1.7.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.7.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.7.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.7.0",
|
||||
"aws-sdk": "^2.1233.0",
|
||||
"@jambonz/verb-specifications": "^0.0.11",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.9.0",
|
||||
"@opentelemetry/instrumentation": "^0.35.0",
|
||||
"@opentelemetry/resources": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.9.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.9.0",
|
||||
"aws-sdk": "^2.1313.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.16",
|
||||
"drachtio-srf": "^4.5.21",
|
||||
"drachtio-fsmrf": "^3.0.20",
|
||||
"drachtio-srf": "^4.5.23",
|
||||
"express": "^4.18.2",
|
||||
"ip": "^1.1.8",
|
||||
"moment": "^2.29.4",
|
||||
"parse-url": "^8.1.0",
|
||||
"pino": "^6.14.0",
|
||||
"pino": "^8.8.0",
|
||||
"polly-ssml-split": "^0.1.0",
|
||||
"proxyquire": "^2.1.3",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"short-uuid": "^4.2.0",
|
||||
"short-uuid": "^4.2.2",
|
||||
"sinon": "^15.0.1",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"undici": "^5.11.0",
|
||||
"undici": "^5.19.1",
|
||||
"uuid-random": "^1.3.2",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"ws": "^8.9.0",
|
||||
"xml2js": "^0.4.23"
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clear-module": "^4.1.2",
|
||||
|
||||
102
test/config-test.js
Normal file
102
test/config-test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -18,30 +18,38 @@ test('creating schema', (t) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('schema and test data successfully created');
|
||||
|
||||
if (process.env.GCP_JSON_KEY && process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
const sql = [];
|
||||
if (process.env.GCP_JSON_KEY) {
|
||||
const google_credential = encrypt(process.env.GCP_JSON_KEY);
|
||||
t.pass('adding google credentials');
|
||||
sql.push(`UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';`);
|
||||
}
|
||||
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
const aws_credential = encrypt(JSON.stringify({
|
||||
access_key_id: process.env.AWS_ACCESS_KEY_ID,
|
||||
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
aws_region: process.env.AWS_REGION
|
||||
}));
|
||||
t.pass('adding aws credentials');
|
||||
sql.push(`UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';`);
|
||||
}
|
||||
if (process.env.MICROSOFT_REGION && process.env.MICROSOFT_API_KEY) {
|
||||
const microsoft_credential = encrypt(JSON.stringify({
|
||||
region: process.env.MICROSOFT_REGION || 'useast',
|
||||
api_key: process.env.MICROSOFT_API_KEY || '1234567890'
|
||||
region: process.env.MICROSOFT_REGION,
|
||||
api_key: process.env.MICROSOFT_API_KEY
|
||||
}));
|
||||
const cmd = `
|
||||
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='${microsoft_credential}' WHERE vendor='microsoft';
|
||||
`;
|
||||
t.pass('adding microsoft credentials');
|
||||
sql.push(`UPDATE speech_credentials SET credential='${microsoft_credential}' WHERE vendor='microsoft';`);
|
||||
}
|
||||
if (sql.length > 0) {
|
||||
const path = `${__dirname}/.creds.sql`;
|
||||
fs.writeFileSync(path, cmd);
|
||||
const cmd = sql.join('\n');
|
||||
fs.writeFileSync(path, sql.join('\n'));
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${path}`, (err, stdout, stderr) => {
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
if (err) return t.end(err);
|
||||
fs.unlinkSync(path)
|
||||
fs.writeFileSync(`${__dirname}/credentials/gcp.json`, process.env.GCP_JSON_KEY);
|
||||
t.pass('set account-level speech credentials');
|
||||
t.end();
|
||||
});
|
||||
|
||||
9
test/data/bad/bad-say-ssml.json
Normal file
9
test/data/bad/bad-say-ssml.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
test/data/good/say-ssml.json
Normal file
9
test/data/good/say-ssml.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,6 +217,7 @@ CREATE TABLE `applications` (
|
||||
`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',
|
||||
`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_language` varchar(12) NOT NULL DEFAULT 'en-US',
|
||||
`speech_synthesis_voice` varchar(64) DEFAULT NULL,
|
||||
@@ -245,13 +246,14 @@ CREATE TABLE `applications` (
|
||||
|
||||
LOCK TABLES `applications` WRITE;
|
||||
/*!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,'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,'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,'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,'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,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 ('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 ('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 ('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 ('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 ('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 */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
@@ -451,6 +453,7 @@ INSERT INTO `phone_numbers` VALUES ('05eeed62-b29b-4679-bf38-d7a4e318be44','1617
|
||||
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 ('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 */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS account_static_ips;
|
||||
|
||||
DROP TABLE IF EXISTS account_limits;
|
||||
|
||||
DROP TABLE IF EXISTS account_products;
|
||||
|
||||
DROP TABLE IF EXISTS account_subscriptions;
|
||||
@@ -18,6 +20,12 @@ DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||
|
||||
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_smpp_gateways;
|
||||
@@ -36,6 +44,8 @@ DROP TABLE IF EXISTS sbc_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS ms_teams_tenants;
|
||||
|
||||
DROP TABLE IF EXISTS service_provider_limits;
|
||||
|
||||
DROP TABLE IF EXISTS signup_history;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_addresses;
|
||||
@@ -69,6 +79,15 @@ private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
|
||||
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
|
||||
(
|
||||
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -123,6 +142,21 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
) 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
|
||||
(
|
||||
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -228,6 +262,15 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
|
||||
PRIMARY KEY (ms_teams_tenant_sid)
|
||||
) 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
|
||||
(
|
||||
email VARCHAR(255) NOT NULL,
|
||||
@@ -283,6 +326,7 @@ email_activation_code VARCHAR(16),
|
||||
email_validated BOOLEAN NOT NULL DEFAULT false,
|
||||
phone_validated 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)
|
||||
);
|
||||
|
||||
@@ -310,9 +354,20 @@ smpp_password VARCHAR(64),
|
||||
smpp_enquire_link_interval INTEGER DEFAULT 0,
|
||||
smpp_inbound_system_id VARCHAR(255),
|
||||
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)
|
||||
) 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
|
||||
(
|
||||
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -330,7 +385,7 @@ PRIMARY KEY (smpp_gateway_sid)
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
phone_number_sid CHAR(36) UNIQUE ,
|
||||
number VARCHAR(32) NOT NULL UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL UNIQUE ,
|
||||
voip_carrier_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
application_sid CHAR(36),
|
||||
@@ -380,6 +435,7 @@ 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_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 ',
|
||||
app_json VARCHAR(16384),
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
@@ -418,6 +474,11 @@ disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
|
||||
trial_end_date DATETIME,
|
||||
deactivated_reason VARCHAR(255),
|
||||
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)
|
||||
) COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
@@ -425,19 +486,23 @@ 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);
|
||||
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_sid_idx ON account_subscriptions (account_sid);
|
||||
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
|
||||
CREATE INDEX call_route_sid_idx ON call_routes (call_route_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 account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_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);
|
||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (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_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);
|
||||
@@ -456,14 +521,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_sid_idx ON account_offers (account_sid);
|
||||
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_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);
|
||||
|
||||
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
|
||||
CREATE INDEX account_sid_idx ON api_keys (account_sid);
|
||||
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_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);
|
||||
@@ -477,44 +542,53 @@ 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);
|
||||
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_6 (account_sid) REFERENCES accounts (account_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 application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
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 smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
|
||||
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_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 service_provider_sid_idx ON speech_credentials (service_provider_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX user_sid_idx ON users (user_sid);
|
||||
CREATE INDEX email_idx ON users (email);
|
||||
CREATE INDEX phone_idx ON users (phone);
|
||||
CREATE INDEX account_sid_idx ON users (account_sid);
|
||||
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
|
||||
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
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 account_sid_idx ON voip_carriers (account_sid);
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON voip_carriers (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 service_provider_sid_idxfk_7 (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);
|
||||
|
||||
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 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);
|
||||
@@ -524,12 +598,12 @@ CREATE INDEX number_idx ON phone_numbers (number);
|
||||
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 account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_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 application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||
|
||||
@@ -545,10 +619,10 @@ CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
|
||||
|
||||
CREATE INDEX application_sid_idx ON applications (application_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE INDEX account_sid_idx ON applications (account_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
|
||||
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_12 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||
|
||||
@@ -564,7 +638,7 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
|
||||
CREATE INDEX account_sid_idx ON accounts (account_sid);
|
||||
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
|
||||
CREATE INDEX service_provider_sid_idx ON accounts (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 service_provider_sid_idxfk_10 (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);
|
||||
|
||||
@@ -572,4 +646,6 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
212
test/dial-tests.js
Normal file
212
test/dial-tests.js
Normal file
@@ -0,0 +1,212 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
drachtio:
|
||||
image: drachtio/drachtio-server:latest
|
||||
restart: always
|
||||
command: drachtio --contact "sip:*;transport=udp,tcp" --address 0.0.0.0 --port 9022
|
||||
command: drachtio --contact "sip:*;transport=udp" --mtu 4096 --address 0.0.0.0 --port 9022
|
||||
ports:
|
||||
- "9060:9022/tcp"
|
||||
networks:
|
||||
|
||||
@@ -206,7 +206,49 @@ test('\'gather\' test - deepgram', async(t) => {
|
||||
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');
|
||||
'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();
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
require('./ws-requestor-unit-test');
|
||||
require('./unit-tests');
|
||||
require('./docker_start');
|
||||
require('./create-test-db');
|
||||
require('./account-validation-tests');
|
||||
require('./dial-tests');
|
||||
require('./webhooks-tests');
|
||||
require('./say-tests');
|
||||
require('./gather-tests');
|
||||
@@ -10,5 +12,7 @@ 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('./docker_stop');
|
||||
require('./docker_stop');
|
||||
149
test/listen-tests.js
Normal file
149
test/listen-tests.js
Normal file
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -200,3 +200,55 @@ test('\'play\' tests with seekOffset and actionHook', async(t) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ test('dropping jambones_test database', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('database successfully dropped');
|
||||
fs.unlinkSync(`${__dirname}/credentials/gcp.json`);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
99
test/scenarios/uac-gather-account-creds-success-send-bye.xml
Normal file
99
test/scenarios/uac-gather-account-creds-success-send-bye.xml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?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>
|
||||
|
||||
86
test/scenarios/uac-invite-expect-183-cancel.xml
Normal file
86
test/scenarios/uac-invite-expect-183-cancel.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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>
|
||||
164
test/scenarios/uas-dial.xml
Normal file
164
test/scenarios/uas-dial.xml
Normal file
@@ -0,0 +1,164 @@
|
||||
<?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>
|
||||
|
||||
@@ -24,13 +24,13 @@ obj.output = () => {
|
||||
return output;
|
||||
};
|
||||
|
||||
obj.sippUac = (file, bindAddress, from='sipp', to='16174000000') => {
|
||||
obj.sippUac = (file, bindAddress, from='sipp', to='16174000000', loop=1) => {
|
||||
const cmd = 'docker';
|
||||
const args = [
|
||||
'run', '-t', '--rm', '--net', `${network}`,
|
||||
'-v', `${__dirname}/scenarios:/tmp/scenarios`,
|
||||
'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
|
||||
'-m', '1',
|
||||
'-m', loop,
|
||||
'-sleep', '250ms',
|
||||
'-nostdin',
|
||||
'-cid_str', `%u-%p@%s-${idx++}`,
|
||||
|
||||
@@ -143,7 +143,7 @@ test('\'transcribe\' test - deepgram', async(t) => {
|
||||
{
|
||||
"verb": "transcribe",
|
||||
"recognizer": {
|
||||
"vendor": "aws",
|
||||
"vendor": "deepgram",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"],
|
||||
"deepgramOptions": {
|
||||
"apiKey": process.env.DEEPGRAM_API_KEY
|
||||
@@ -160,6 +160,47 @@ test('\'transcribe\' test - deepgram', async(t) => {
|
||||
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}`);
|
||||
|
||||
@@ -44,10 +44,25 @@ test('unit tests', (t) => {
|
||||
|
||||
task = makeTask(logger, require('./data/good/say-text-array'));
|
||||
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 normalize = require('../lib/utils/normalize-jambones');
|
||||
normalize(logger, alt).forEach((t) => {
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
normalizeJambones(logger, alt).forEach((t) => {
|
||||
const task = makeTask(logger, t);
|
||||
});
|
||||
t.pass('alternate syntax works');
|
||||
@@ -62,4 +77,4 @@ const errMissingProperty = () => makeTask(logger, require('./data/bad/missing-re
|
||||
const errInvalidType = () => makeTask(logger, require('./data/bad/invalid-type'));
|
||||
const errBadEnum = () => makeTask(logger, require('./data/bad/bad-enum'));
|
||||
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'));
|
||||
@@ -1,14 +1,54 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const Websocket = require('ws');
|
||||
const listenPort = process.env.HTTP_PORT || 3000;
|
||||
let json_mapping = new Map();
|
||||
let hook_mapping = new Map();
|
||||
let ws_packet_count = new Map();
|
||||
let ws_metadata = new Map();
|
||||
|
||||
app.listen(listenPort, () => {
|
||||
/** websocket server for listen audio */
|
||||
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', () => {
|
||||
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}`);
|
||||
});
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
console.log('received upgrade request');
|
||||
wsServer.handleUpgrade(request, socket, head, (socket) => {
|
||||
wsServer.emit('connection', socket, request);
|
||||
});
|
||||
});
|
||||
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
@@ -20,6 +60,7 @@ app.use(express.json());
|
||||
app.all('/', (req, res) => {
|
||||
console.log(req.body, 'POST /');
|
||||
const key = req.body.from
|
||||
addRequestToMap(key, req, hook_mapping);
|
||||
return getJsonFromMap(key, req, res);
|
||||
});
|
||||
|
||||
@@ -96,6 +137,27 @@ app.get('/lastRequest/:key', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
*/
|
||||
|
||||
29
test/webhook/package-lock.json
generated
29
test/webhook/package-lock.json
generated
@@ -9,7 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -587,6 +588,26 @@
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz",
|
||||
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -1013,6 +1034,12 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz",
|
||||
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"author": "Dave Horton",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
97
test/ws-mock.js
Normal file
97
test/ws-mock.js
Normal file
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
198
test/ws-requestor-unit-test.js
Normal file
198
test/ws-requestor-unit-test.js
Normal file
@@ -0,0 +1,198 @@
|
||||
const test = require('tape');
|
||||
const sinon = require('sinon');
|
||||
const proxyquire = require("proxyquire");
|
||||
proxyquire.noCallThru();
|
||||
const MockWebsocket = require('./ws-mock')
|
||||
const logger = require('pino')({level: process.env.JAMBONES_LOGLEVEL || 'error'});
|
||||
|
||||
const BaseRequestor = proxyquire(
|
||||
"../lib/utils/base-requestor",
|
||||
{
|
||||
"../../": {
|
||||
srf: {
|
||||
locals: {
|
||||
stats: {
|
||||
histogram: () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@jambonz/time-series": sinon.stub()
|
||||
}
|
||||
);
|
||||
|
||||
const WsRequestor = proxyquire(
|
||||
"../lib/utils/ws-requestor",
|
||||
{
|
||||
"./base-requestor": BaseRequestor,
|
||||
"ws": MockWebsocket
|
||||
}
|
||||
);
|
||||
|
||||
test('ws success', async (t) => {
|
||||
// GIVEN
|
||||
|
||||
const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]';
|
||||
const ws_response = {
|
||||
action: ['connect'],
|
||||
body: json
|
||||
}
|
||||
const call_sid = 'ws_success';
|
||||
|
||||
MockWebsocket.addJsonMapping(call_sid, ws_response);
|
||||
|
||||
const hook = {
|
||||
url: 'ws://localhost:3000',
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
const params = {
|
||||
callSid: call_sid
|
||||
}
|
||||
|
||||
// WHEN
|
||||
|
||||
const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret");
|
||||
const result = await requestor.request('session:new',hook, params, {});
|
||||
|
||||
// THEN
|
||||
t.ok(result == json,'ws successfully sent session:new and got initial jambonz app');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('ws close success reconnect', async (t) => {
|
||||
// GIVEN
|
||||
|
||||
const call_sid = 'ws_closed'
|
||||
const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]';
|
||||
const ws_response = {
|
||||
action: ['close', 'connect'],
|
||||
body: json
|
||||
}
|
||||
MockWebsocket.addJsonMapping(call_sid, ws_response);
|
||||
|
||||
const hook = {
|
||||
url: 'ws://localhost:3000',
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
const params = {
|
||||
callSid: call_sid
|
||||
}
|
||||
|
||||
// WHEN
|
||||
|
||||
const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret");
|
||||
const result = await requestor.request('session:new',hook, params, {});
|
||||
|
||||
// THEN
|
||||
t.ok(result == json,'ws successfully reconnect after close from far end');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
|
||||
test('ws response error 1000', async (t) => {
|
||||
// GIVEN
|
||||
|
||||
const call_sid = 'ws_terminated'
|
||||
const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]';
|
||||
const ws_response = {
|
||||
action: ['terminate'],
|
||||
body: json
|
||||
}
|
||||
MockWebsocket.addJsonMapping(call_sid, ws_response);
|
||||
|
||||
const hook = {
|
||||
url: 'ws://localhost:3000',
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
const params = {
|
||||
callSid: call_sid
|
||||
}
|
||||
|
||||
// WHEN
|
||||
|
||||
const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret");
|
||||
try {
|
||||
await requestor.request('session:new',hook, params, {});
|
||||
}
|
||||
catch (err) {
|
||||
// THEN
|
||||
t.ok(err.startsWith('timeout from far end for msgid'), 'ws does not reconnect if far end closes gracefully');
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
|
||||
test('ws response error', async (t) => {
|
||||
// GIVEN
|
||||
|
||||
const call_sid = 'ws_error'
|
||||
const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]';
|
||||
const ws_response = {
|
||||
action: ['error'],
|
||||
body: json
|
||||
}
|
||||
MockWebsocket.addJsonMapping(call_sid, ws_response);
|
||||
|
||||
const hook = {
|
||||
url: 'ws://localhost:3000',
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
const params = {
|
||||
callSid: call_sid
|
||||
}
|
||||
|
||||
// WHEN
|
||||
|
||||
const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret");
|
||||
try {
|
||||
await requestor.request('session:new',hook, params, {});
|
||||
}
|
||||
catch (err) {
|
||||
// THEN
|
||||
t.ok(err.startsWith('timeout from far end for msgid'), 'ws does not reconnect if far end closes gracefully');
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
|
||||
test('ws unexpected-response', async (t) => {
|
||||
// GIVEN
|
||||
|
||||
const call_sid = 'ws_unexpected-response'
|
||||
const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]';
|
||||
const ws_response = {
|
||||
action: ['unexpected-response'],
|
||||
body: json
|
||||
}
|
||||
MockWebsocket.addJsonMapping(call_sid, ws_response);
|
||||
|
||||
const hook = {
|
||||
url: 'ws://localhost:3000',
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
const params = {
|
||||
callSid: call_sid
|
||||
}
|
||||
|
||||
// WHEN
|
||||
|
||||
const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret");
|
||||
try {
|
||||
await requestor.request('session:new',hook, params, {});
|
||||
}
|
||||
catch (err) {
|
||||
// THEN
|
||||
t.ok(err.code = 'ERR_ASSERTION', 'ws does not reconnect if far end closes gracefully');
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user