Compare commits

..

23 Commits

Author SHA1 Message Date
Dave Horton
7c85d6aeca bugfix: db caching had side affects of using closed http requestors 2022-12-13 14:52:38 -05:00
Dave Horton
cc87b205a2 update gh actions 2022-12-10 15:32:08 -05:00
Dave Horton
fff556a6c8 update drachtio-fsmrf and srf with more efficient freeswitch call setup 2022-12-10 15:28:23 -05:00
Dave Horton
bb4ca8e467 bugfix: when handing over from wss to http close the wss socket 2022-12-09 10:55:22 -05:00
Dave Horton
46302703da further fix for #192, also bug fix for starting with a ws connection and switching to webhooks later in the same call 2022-12-05 10:53:41 -05:00
Dave Horton
c728417581 bugfix #192: config with dtmf only followed later by gather with speech not working 2022-12-01 14:06:29 -05:00
Dave Horton
8853f84f01 add custom header on Refer indicating whether sbc-inbound should fix up the Refer-To 2022-11-30 12:50:54 -05:00
Dave Horton
665d26b6fb bugfix: continuous asr timer in gather should not start until transcript is received 2022-11-29 11:37:41 -05:00
Dave Horton
d69c773de0 include service_provider_sid in call webhook 2022-11-29 11:27:20 -05:00
Dave Horton
21eaa442b2 add recognizer.azureServiceEndpoint for custom azure voices 2022-11-25 10:46:47 -05:00
Dave Horton
6484086222 feature: return transcript faster if we get an exact match to a provided hint on an interim transcript (requires env JAMBONZ_GATHER_EARLY_HINTS_MATCH=1) 2022-11-25 08:15:18 -05:00
Dave Horton
01645df920 error handling in amd 2022-11-22 15:40:26 -05:00
Guilherme Rauen
b2363b09c1 update node image to the latest and most secure (#189)
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2022-11-11 17:45:26 -05:00
Dave Horton
c11d892f0a bugfix: microsoft tts voice was not being sent in tts request, resulting in a default voice being selected 2022-11-10 13:00:59 -05:00
Dave Horton
9fd116b05f fix for #186: unhandled error when amd webhook returns non-success status code 2022-11-05 10:27:00 -04:00
Dave Horton
19098aee98 fixes for custom voice testing in azure 2022-11-04 09:36:44 -04:00
Dave Horton
d15dbf7f5a update to synthAudio with support for Azure custom voices 2022-11-04 08:27:09 -04:00
Dave Horton
824f983955 update deps 2022-11-02 13:40:25 -04:00
Dave Horton
7c76bc52f6 update to db-helpers with caching fix 2022-11-01 20:57:18 -04:00
Dave Horton
bfc8a99950 bugfix: ws error max connections error causes a crash 2022-11-01 11:33:03 -04:00
Dave Horton
9097c6d6ac bugfix when running multiple instances in EC2 2022-10-31 14:42:53 -04:00
Dave Horton
15b2fdd5a8 update to db-helpers@0.7.0 with caching option 2022-10-31 11:43:07 -04:00
Dave Horton
979e17c814 add support for Azure audio logging in gather and transcribe 2022-10-31 11:08:16 -04:00
91 changed files with 3699 additions and 12482 deletions

View File

@@ -1,6 +1,7 @@
name: CI name: CI
on: [push, pull_request] on:
push:
jobs: jobs:
build: build:
@@ -9,7 +10,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: lts/*
- run: npm ci - run: npm ci
- run: npm run jslint - run: npm run jslint
- run: docker pull drachtio/sipp - run: docker pull drachtio/sipp
@@ -19,5 +20,3 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }} AWS_REGION: ${{ secrets.AWS_REGION }}
MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }}

View File

@@ -2,10 +2,16 @@ name: Docker
on: on:
push: push:
# Publish `main` as Docker `latest` image.
branches: branches:
- main - main
# Publish `v1.2.3` tags as releases.
tags: tags:
- '*' - v*
env:
IMAGE_NAME: feature-server
jobs: jobs:
push: push:
@@ -14,41 +20,32 @@ jobs:
if: github.event_name == 'push' if: github.event_name == 'push'
steps: steps:
- name: Checkout code - uses: actions/checkout@v3
uses: actions/checkout@v3
- name: prepare tag - name: Build image
id: prepare_tag run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: | run: |
IMAGE_ID=jambonz/feature-server IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Strip git ref prefix from version # Change all uppercase to lowercase
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip "v" prefix from tag name # Strip git ref prefix from version
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Use Docker `latest` tag convention # Strip "v" prefix from tag name
[ "$VERSION" == "main" ] && VERSION=latest [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
echo IMAGE_ID=$IMAGE_ID # Use Docker `latest` tag convention
echo VERSION=$VERSION [ "$VERSION" == "main" ] && VERSION=latest
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT echo IMAGE_ID=$IMAGE_ID
echo "version=$VERSION" >> $GITHUB_OUTPUT echo VERSION=$VERSION
- name: Login to Docker Hub docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
uses: docker/login-action@v2 docker push $IMAGE_ID:$VERSION
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF

3
.gitignore vendored
View File

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

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base FROM --platform=linux/amd64 node:18.12.1-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3 RUN apk --update --no-cache add --virtual .builds-deps build-base python3

View File

@@ -18,10 +18,8 @@ Configuration is provided via environment variables:
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes| |DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|DRACHTIO_SECRET| shared secret|yes| |DRACHTIO_SECRET| shared secret|yes|
|ENABLE_METRICS| if 1, metrics will be generated|no| |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| |GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes| |HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no|
|JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes| |JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no| |JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|JAMBONES_MYSQL_HOST| mysql host|yes| |JAMBONES_MYSQL_HOST| mysql host|yes|
@@ -89,4 +87,4 @@ module.exports = {
#### Running the test suite #### Running the test suite
Please [see this](./docs/contributing.md#run-the-regression-test-suite). Please [see this]](./docs/contributing.md#run-the-regression-test-suite).

69
app.js
View File

@@ -1,28 +1,21 @@
const { const assert = require('assert');
DRACHTIO_PORT, assert.ok(process.env.JAMBONES_MYSQL_HOST &&
DRACHTIO_HOST, process.env.JAMBONES_MYSQL_USER &&
DRACHTIO_SECRET, process.env.JAMBONES_MYSQL_PASSWORD &&
JAMBONES_OTEL_SERVICE_NAME, process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
JAMBONES_LOGLEVEL, assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
JAMBONES_CLUSTER_ID, assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
JAMBONZ_CLEANUP_INTERVAL_MINS, assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
getCleanupIntervalMins, assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
K8S, assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
NODE_ENV,
checkEnvs,
} = require('./lib/config');
checkEnvs();
const Srf = require('drachtio-srf'); const Srf = require('drachtio-srf');
const srf = new Srf(); const srf = new Srf();
const tracer = require('./tracer')(JAMBONES_OTEL_SERVICE_NAME); const tracer = require('./tracer')(process.env.JAMBONES_OTEL_SERVICE_NAME || 'jambonz-feature-server');
const api = require('@opentelemetry/api'); const api = require('@opentelemetry/api');
srf.locals = {...srf.locals, otel: {tracer, api}}; srf.locals = {...srf.locals, otel: {tracer, api}};
const opts = { const opts = {level: process.env.JAMBONES_LOGLEVEL || 'info'};
level: JAMBONES_LOGLEVEL
};
const pino = require('pino'); const pino = require('pino');
const logger = pino(opts, pino.destination({sync: false})); const logger = pino(opts, pino.destination({sync: false}));
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants'); const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
@@ -42,8 +35,8 @@ const {
const InboundCallSession = require('./lib/session/inbound-call-session'); const InboundCallSession = require('./lib/session/inbound-call-session');
const SipRecCallSession = require('./lib/session/siprec-call-session'); const SipRecCallSession = require('./lib/session/siprec-call-session');
if (DRACHTIO_HOST) { if (process.env.DRACHTIO_HOST) {
srf.connect({host: DRACHTIO_HOST, port: DRACHTIO_PORT, secret: DRACHTIO_SECRET }); srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
srf.on('connect', (err, hp) => { srf.on('connect', (err, hp) => {
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop()); const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
srf.locals.localSipAddress = `${arr[2]}`; srf.locals.localSipAddress = `${arr[2]}`;
@@ -51,10 +44,10 @@ if (DRACHTIO_HOST) {
}); });
} }
else { else {
logger.info(`listening for drachtio requests on port ${DRACHTIO_PORT}`); logger.info(`listening for drachtio requests on port ${process.env.DRACHTIO_PORT}`);
srf.listen({port: DRACHTIO_PORT, secret: DRACHTIO_SECRET}); srf.listen({port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET});
} }
if (NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
srf.on('error', (err) => { srf.on('error', (err) => {
logger.info(err, 'Error connecting to drachtio'); logger.info(err, 'Error connecting to drachtio');
}); });
@@ -113,33 +106,19 @@ const disconnect = () => {
}); });
}; };
process.on('SIGUSR2', handle);
process.on('SIGTERM', handle); process.on('SIGTERM', handle);
function handle(signal) { function handle(signal) {
const {removeFromSet} = srf.locals.dbHelpers; const {removeFromSet} = srf.locals.dbHelpers;
srf.locals.disabled = true; const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
logger.info(`got signal ${signal}`); logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
const setName = `${(JAMBONES_CLUSTER_ID || 'default')}:active-fs`; removeFromSet(setName, srf.locals.localSipAddress);
const fsServiceUrlSetName = `${(JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
if (setName && srf.locals.localSipAddress) {
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
removeFromSet(setName, srf.locals.localSipAddress);
}
if (fsServiceUrlSetName && srf.locals.serviceUrl) {
logger.info(`got signal ${signal}, removing ${srf.locals.serviceUrl} from set ${fsServiceUrlSetName}`);
removeFromSet(fsServiceUrlSetName, srf.locals.serviceUrl);
}
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID); removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
if (K8S) { srf.locals.disabled = true;
srf.locals.lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
}
if (getCount() === 0) {
logger.info('no calls in progress, exiting');
process.exit(0);
}
} }
if (JAMBONZ_CLEANUP_INTERVAL_MINS) { if (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS) {
const {clearFiles} = require('./lib/utils/cron-jobs'); const {clearFiles} = require('./lib/utils/cron-jobs');
/* cleanup orphaned files or channels every so often */ /* cleanup orphaned files or channels every so often */
@@ -149,7 +128,7 @@ if (JAMBONZ_CLEANUP_INTERVAL_MINS) {
} catch (err) { } catch (err) {
logger.error({err}, 'app.js: error clearing files'); logger.error({err}, 'app.js: error clearing files');
} }
}, getCleanupIntervalMins()); }, 1000 * 60 * (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS || 60));
} }
module.exports = {srf, logger, disconnect}; module.exports = {srf, logger, disconnect};

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const bent = require('bent'); const bent = require('bent');
const getJSON = bent('json'); const getJSON = bent('json');
const {PORT} = require('../lib/config') const PORT = process.env.HTTP_PORT || 3000;
const sleep = (ms) => { const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));

View File

@@ -5,11 +5,14 @@
"at the tone", "at the tone",
"leave a message", "leave a message",
"leave me a message", "leave me a message",
"not available", "not available right now",
"not available to take your call",
"can't take your call", "can't take your call",
"will get back to you", "I will get back to you",
"I'll get back to you", "I'll get back to you",
"we are unable" "we will get back to you",
"we are unable",
"we are not available"
], ],
"es-ES": [ "es-ES": [
"le pasamos la llamada", "le pasamos la llamada",
@@ -45,18 +48,5 @@
"ens posarem en contacto", "ens posarem en contacto",
"ara no estem disponibles", "ara no estem disponibles",
"no hi som" "no hi som"
],
"de-DE": [
"nicht erreichbar",
"nnruf wurde weitergeleitet",
"beim piepsen",
"am ton",
"eine nachricht hinterlassen",
"hinterlasse mir eine Nachricht",
"nicht verfügbar",
"kann ihren anruf nicht entgegennehmen",
"wird sich bei Ihnen melden",
"ich melde mich bei dir",
"wir können nicht"
] ]
} }

View File

@@ -1,229 +0,0 @@
const assert = require('assert');
const checkEnvs = () => {
assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_USER &&
process.env.JAMBONES_MYSQL_PASSWORD &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
if (process.env.JAMBONES_REDIS_SENTINELS) {
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
} else {
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');
};
const NODE_ENV = process.env.NODE_ENV;
/* database mySQL */
const JAMBONES_MYSQL_HOST = process.env.JAMBONES_MYSQL_HOST;
const JAMBONES_MYSQL_USER = process.env.JAMBONES_MYSQL_USER;
const JAMBONES_MYSQL_PASSWORD = process.env.JAMBONES_MYSQL_PASSWORD;
const JAMBONES_MYSQL_DATABASE = process.env.JAMBONES_MYSQL_DATABASE;
const JAMBONES_MYSQL_PORT = parseInt(process.env.JAMBONES_MYSQL_PORT, 10) || 3306;
const JAMBONES_MYSQL_REFRESH_TTL = process.env.JAMBONES_MYSQL_REFRESH_TTL;
const JAMBONES_MYSQL_CONNECTION_LIMIT = parseInt(process.env.JAMBONES_MYSQL_CONNECTION_LIMIT, 10) || 10;
/* redis */
const JAMBONES_REDIS_HOST = process.env.JAMBONES_REDIS_HOST;
const JAMBONES_REDIS_PORT = parseInt(process.env.JAMBONES_REDIS_PORT, 10) || 6379;
/* gather and hints */
const JAMBONES_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONES_GATHER_EARLY_HINTS_MATCH;
const JAMBONZ_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH;
const JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS = process.env.JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS;
const SMPP_URL = process.env.SMPP_URL;
/* drachtio */
const DRACHTIO_PORT = process.env.DRACHTIO_PORT;
const DRACHTIO_HOST = process.env.DRACHTIO_HOST;
const DRACHTIO_SECRET = process.env.DRACHTIO_SECRET;
/* freeswitch */
const JAMBONES_API_BASE_URL = process.env.JAMBONES_API_BASE_URL;
const JAMBONES_FREESWITCH = process.env.JAMBONES_FREESWITCH;
const JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS = parseInt(process.env.JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS, 10)
|| 180;
const JAMBONES_SBCS = process.env.JAMBONES_SBCS;
/* websockets */
const JAMBONES_WS_HANDSHAKE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS, 10) || 1500;
const JAMBONES_WS_MAX_PAYLOAD = parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD, 10) || 24 * 1024;
const JAMBONES_WS_PING_INTERVAL_MS = parseInt(process.env.JAMBONES_WS_PING_INTERVAL_MS, 10) || 0;
const MAX_RECONNECTS = 5;
const RESPONSE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT, 10) || 5000;
const JAMBONES_NETWORK_CIDR = process.env.JAMBONES_NETWORK_CIDR;
const JAMBONES_TIME_SERIES_HOST = process.env.JAMBONES_TIME_SERIES_HOST;
const JAMBONES_CLUSTER_ID = process.env.JAMBONES_CLUSTER_ID || 'default';
const JAMBONES_ESL_LISTEN_ADDRESS = process.env.JAMBONES_ESL_LISTEN_ADDRESS;
/* tracing */
const JAMBONES_OTEL_ENABLED = process.env.JAMBONES_OTEL_ENABLED;
const JAMBONES_OTEL_SERVICE_NAME = process.env.JAMBONES_OTEL_SERVICE_NAME || 'jambonz-feature-server';
const OTEL_EXPORTER_JAEGER_AGENT_HOST = process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST;
const OTEL_EXPORTER_JAEGER_ENDPOINT = process.env.OTEL_EXPORTER_JAEGER_ENDPOINT;
const OTEL_EXPORTER_ZIPKIN_URL = process.env.OTEL_EXPORTER_ZIPKIN_URL;
const OTEL_EXPORTER_COLLECTOR_URL = process.env.OTEL_EXPORTER_COLLECTOR_URL;
const JAMBONES_LOGLEVEL = process.env.JAMBONES_LOGLEVEL || 'info';
const JAMBONES_INJECT_CONTENT = process.env.JAMBONES_INJECT_CONTENT;
const PORT = parseInt(process.env.HTTP_PORT, 10) || 3000;
const HTTP_PORT_MAX = parseInt(process.env.HTTP_PORT_MAX, 10);
const K8S = process.env.K8S;
const K8S_SBC_SIP_SERVICE_NAME = process.env.K8S_SBC_SIP_SERVICE_NAME;
const JAMBONES_SUBNET = process.env.JAMBONES_SUBNET;
/* clean up */
const JAMBONZ_CLEANUP_INTERVAL_MINS = process.env.JAMBONZ_CLEANUP_INTERVAL_MINS;
const getCleanupIntervalMins = () => {
const interval = parseInt(JAMBONZ_CLEANUP_INTERVAL_MINS, 10) || 60;
return 1000 * 60 * interval;
};
/* speech vendors */
const AWS_REGION = process.env.AWS_REGION;
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
const AWS_SNS_PORT = parseInt(process.env.AWS_SNS_PORT, 10) || 3001;
const AWS_SNS_TOPIC_ARM = process.env.AWS_SNS_TOPIC_ARM;
const AWS_SNS_PORT_MAX = parseInt(process.env.AWS_SNS_PORT_MAX, 10) || 3005;
const GCP_JSON_KEY = process.env.GCP_JSON_KEY;
const MICROSOFT_REGION = process.env.MICROSOFT_REGION;
const MICROSOFT_API_KEY = process.env.MICROSOFT_API_KEY;
const SONIOX_API_KEY = process.env.SONIOX_API_KEY;
const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY;
const ANCHOR_MEDIA_ALWAYS = process.env.ANCHOR_MEDIA_ALWAYS;
const VMD_HINTS_FILE = process.env.VMD_HINTS_FILE;
/* security, secrets */
const LEGACY_CRYPTO = !!process.env.LEGACY_CRYPTO;
const JWT_SECRET = process.env.JWT_SECRET;
const ENCRYPTION_SECRET = process.env.ENCRYPTION_SECRET;
/* HTTP/1 pool dispatcher */
const HTTP_POOL = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
const HTTP_POOLSIZE = parseInt(process.env.HTTP_POOLSIZE, 10) || 10;
const HTTP_PIPELINING = parseInt(process.env.HTTP_PIPELINING, 10) || 1;
const HTTP_TIMEOUT = 10000;
const OPTIONS_PING_INTERVAL = parseInt(process.env.OPTIONS_PING_INTERVAL, 10) || 30000;
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
let host, port = 26379;
if (sentinel.includes(':')) {
const arr = sentinel.split(':');
host = arr[0];
port = parseInt(arr[1], 10);
} else {
host = sentinel;
}
return {host, port};
}),
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
}),
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
})
} : null;
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL;
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME;
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD;
module.exports = {
JAMBONES_MYSQL_HOST,
JAMBONES_MYSQL_USER,
JAMBONES_MYSQL_PASSWORD,
JAMBONES_MYSQL_DATABASE,
JAMBONES_MYSQL_REFRESH_TTL,
JAMBONES_MYSQL_CONNECTION_LIMIT,
JAMBONES_MYSQL_PORT,
DRACHTIO_PORT,
DRACHTIO_HOST,
DRACHTIO_SECRET,
JAMBONES_GATHER_EARLY_HINTS_MATCH,
JAMBONZ_GATHER_EARLY_HINTS_MATCH,
JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS,
JAMBONES_FREESWITCH,
JAMBONES_REDIS_HOST,
JAMBONES_REDIS_PORT,
JAMBONES_REDIS_SENTINELS,
SMPP_URL,
JAMBONES_NETWORK_CIDR,
JAMBONES_API_BASE_URL,
JAMBONES_TIME_SERIES_HOST,
JAMBONES_INJECT_CONTENT,
JAMBONES_ESL_LISTEN_ADDRESS,
JAMBONES_SBCS,
JAMBONES_OTEL_ENABLED,
JAMBONES_OTEL_SERVICE_NAME,
OTEL_EXPORTER_JAEGER_AGENT_HOST,
OTEL_EXPORTER_JAEGER_ENDPOINT,
OTEL_EXPORTER_ZIPKIN_URL,
OTEL_EXPORTER_COLLECTOR_URL,
JAMBONES_LOGLEVEL,
JAMBONES_CLUSTER_ID,
PORT,
HTTP_PORT_MAX,
K8S,
K8S_SBC_SIP_SERVICE_NAME,
JAMBONES_SUBNET,
NODE_ENV,
JAMBONZ_CLEANUP_INTERVAL_MINS,
getCleanupIntervalMins,
checkEnvs,
AWS_REGION,
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_SNS_PORT,
AWS_SNS_TOPIC_ARM,
AWS_SNS_PORT_MAX,
ANCHOR_MEDIA_ALWAYS,
VMD_HINTS_FILE,
JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS,
LEGACY_CRYPTO,
JWT_SECRET,
ENCRYPTION_SECRET,
HTTP_POOL,
HTTP_POOLSIZE,
HTTP_PIPELINING,
HTTP_TIMEOUT,
OPTIONS_PING_INTERVAL,
RESPONSE_TIMEOUT_MS,
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
JAMBONES_WS_MAX_PAYLOAD,
JAMBONES_WS_PING_INTERVAL_MS,
MAX_RECONNECTS,
GCP_JSON_KEY,
MICROSOFT_REGION,
MICROSOFT_API_KEY,
SONIOX_API_KEY,
DEEPGRAM_API_KEY,
JAMBONZ_RECORD_WS_BASE_URL,
JAMBONZ_RECORD_WS_USERNAME,
JAMBONZ_RECORD_WS_PASSWORD
};

View File

@@ -3,7 +3,7 @@ const makeTask = require('../../tasks/make_task');
const RestCallSession = require('../../session/rest-call-session'); const RestCallSession = require('../../session/rest-call-session');
const CallInfo = require('../../session/call-info'); const CallInfo = require('../../session/call-info');
const {CallDirection, CallStatus} = require('../../utils/constants'); const {CallDirection, CallStatus} = require('../../utils/constants');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const SipError = require('drachtio-srf').SipError; const SipError = require('drachtio-srf').SipError;
const sysError = require('./error'); const sysError = require('./error');
const HttpRequestor = require('../../utils/http-requestor'); const HttpRequestor = require('../../utils/http-requestor');
@@ -19,23 +19,14 @@ router.post('/', async(req, res) => {
logger.debug({body: req.body}, 'got createCall request'); logger.debug({body: req.body}, 'got createCall request');
try { try {
let uri, cs, to; let uri, cs, to;
// app_json is creaeted by only api-server.
// if it available, take it and delete before creating task
const app_json = req.body.app_json;
delete req.body.app_json;
const restDial = makeTask(logger, {'rest:dial': req.body}); const restDial = makeTask(logger, {'rest:dial': req.body});
restDial.appJson = app_json; const {lookupAccountDetails} = dbUtils(logger, srf);
const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
const {
lookupAppBySid
} = srf.locals.dbHelpers;
const {getSBC, getFreeswitch} = srf.locals; const {getSBC, getFreeswitch} = srf.locals;
const sbcAddress = getSBC(); const sbcAddress = getSBC();
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation'); if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
const target = restDial.to; const target = restDial.to;
const opts = { const opts = {
callingNumber: restDial.from, callingNumber: restDial.from,
...(restDial.callerName && {callingName: restDial.callerName}),
headers: req.body.headers || {} headers: req.body.headers || {}
}; };
@@ -44,9 +35,6 @@ router.post('/', async(req, res) => {
const account = await lookupAccountBySid(req.body.account_sid); const account = await lookupAccountBySid(req.body.account_sid);
const accountInfo = await lookupAccountDetails(req.body.account_sid); const accountInfo = await lookupAccountDetails(req.body.account_sid);
const callSid = uuidv4(); const callSid = uuidv4();
const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null;
const record_all_calls = account.record_all_calls || (application && application.record_all_calls);
const recordOutputFormat = account.record_format || 'mp3';
opts.headers = { opts.headers = {
...opts.headers, ...opts.headers,
@@ -54,9 +42,7 @@ router.post('/', async(req, res) => {
'X-Jambonz-FS-UUID': srf.locals.fsUUID, 'X-Jambonz-FS-UUID': srf.locals.fsUUID,
'X-Call-Sid': callSid, 'X-Call-Sid': callSid,
'X-Account-Sid': accountSid, 'X-Account-Sid': accountSid,
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}), ...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost})
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat})
}; };
switch (target.type) { switch (target.type) {
@@ -90,6 +76,7 @@ router.post('/', async(req, res) => {
} }
if (target.type === 'phone' && target.trunk) { if (target.type === 'phone' && target.trunk) {
const {lookupCarrier} = dbUtils(this.logger, srf);
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk); const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
logger.info( logger.info(
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`); `createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
@@ -98,21 +85,6 @@ router.post('/', async(req, res) => {
} }
} }
/**
* trunk isn't specified,
* check if from-number matches any existing numbers on Jambonz
* */
if (target.type === 'phone' && !target.trunk) {
const str = restDial.from || '';
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
logger.info(
`createCall: selected ${voip_carrier_sid} for requested phone number: ${callingNumber || 'unspecified'})`);
if (voip_carrier_sid) {
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
}
}
/* create endpoint for outdial */ /* create endpoint for outdial */
const ms = getFreeswitch(); const ms = getFreeswitch();
if (!ms) throw new Error('no available Freeswitch for outbound call creation'); if (!ms) throw new Error('no available Freeswitch for outbound call creation');
@@ -132,7 +104,7 @@ router.post('/', async(req, res) => {
proxy: `sip:${sbcAddress}`, proxy: `sip:${sbcAddress}`,
localSdp: ep.local.sdp localSdp: ep.local.sdp
}); });
if (target.auth) opts.auth = target.auth; if (target.auth) opts.auth = this.target.auth;
/** /**
@@ -164,7 +136,7 @@ router.post('/', async(req, res) => {
} }
else if (!app.notifier) { else if (!app.notifier) {
logger.debug('creating null call status hook'); logger.debug('creating null call status hook');
app.notifier = {request: () => {}, close: () => {}}; app.notifier = {request: () => {}};
} }
/* now launch the outdial */ /* now launch the outdial */

View File

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

View File

@@ -4,7 +4,7 @@ const WsRequestor = require('../../utils/ws-requestor');
const CallInfo = require('../../session/call-info'); const CallInfo = require('../../session/call-info');
const {CallDirection} = require('../../utils/constants'); const {CallDirection} = require('../../utils/constants');
const SmsSession = require('../../session/sms-call-session'); const SmsSession = require('../../session/sms-call-session');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../../utils/normalize-jambones');
const {TaskPreconditions} = require('../../utils/constants'); const {TaskPreconditions} = require('../../utils/constants');
const makeTask = require('../../tasks/make_task'); const makeTask = require('../../tasks/make_task');

View File

@@ -41,7 +41,7 @@ function retrieveCallSession(callSid, opts) {
router.post('/:callSid', async(req, res) => { router.post('/:callSid', async(req, res) => {
const logger = req.app.locals.logger; const logger = req.app.locals.logger;
const callSid = req.params.callSid; const callSid = req.params.callSid;
logger.debug({body: req.body}, 'got upateCall request'); logger.debug({body: req.body}, 'got updateCall request');
try { try {
const cs = retrieveCallSession(callSid, req.body); const cs = retrieveCallSession(callSid, req.body);
if (!cs) { if (!cs) {

View File

@@ -1,4 +1,4 @@
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const {CallDirection, AllowedSipRecVerbs} = require('./utils/constants'); const {CallDirection, AllowedSipRecVerbs} = require('./utils/constants');
const {parseSiprecPayload} = require('./utils/siprec-utils'); const {parseSiprecPayload} = require('./utils/siprec-utils');
const CallInfo = require('./session/call-info'); const CallInfo = require('./session/call-info');
@@ -6,13 +6,10 @@ const HttpRequestor = require('./utils/http-requestor');
const WsRequestor = require('./utils/ws-requestor'); const WsRequestor = require('./utils/ws-requestor');
const makeTask = require('./tasks/make_task'); const makeTask = require('./tasks/make_task');
const parseUri = require('drachtio-srf').parseUri; const parseUri = require('drachtio-srf').parseUri;
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('./utils/normalize-jambones');
const dbUtils = require('./utils/db-utils'); const dbUtils = require('./utils/db-utils');
const RootSpan = require('./utils/call-tracer'); const RootSpan = require('./utils/call-tracer');
const listTaskNames = require('./utils/summarize-tasks'); const listTaskNames = require('./utils/summarize-tasks');
const {
JAMBONES_MYSQL_REFRESH_TTL
} = require('./config');
module.exports = function(srf, logger) { module.exports = function(srf, logger) {
const { const {
@@ -30,11 +27,7 @@ module.exports = function(srf, logger) {
function initLocals(req, res, next) { function initLocals(req, res, next) {
const callId = req.get('Call-ID'); const callId = req.get('Call-ID');
logger.info({ logger.info({callId}, 'new incoming call');
callId,
callingNumber: req.callingNumber,
calledNumber: req.calledNumber
}, 'new incoming call');
if (!req.has('X-Account-Sid')) { if (!req.has('X-Account-Sid')) {
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header'); logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
return res.send(500); return res.send(500);
@@ -49,16 +42,7 @@ module.exports = function(srf, logger) {
} }
if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User'); 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-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(); next();
} }
@@ -106,10 +90,8 @@ module.exports = function(srf, logger) {
.find((p) => p.type === 'application/sdp') .find((p) => p.type === 'application/sdp')
.content; .content;
const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger); const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger);
if (!req.locals.calledNumber && !req.locals.calledNumber) { req.locals.calledNumber = metadata.caller.number;
req.locals.calledNumber = metadata.caller.number; req.locals.callingNumber = metadata.callee.number;
req.locals.callingNumber = metadata.callee.number;
}
req.locals = { req.locals = {
...req.locals, ...req.locals,
siprec: { siprec: {
@@ -245,12 +227,11 @@ module.exports = function(srf, logger) {
*/ */
/* allow for caching data - when caching treat retrieved data as immutable */ /* allow for caching data - when caching treat retrieved data as immutable */
const app2 = JAMBONES_MYSQL_REFRESH_TTL ? JSON.parse(JSON.stringify(app)) : app; const app2 = process.env.JAMBONES_MYSQL_REFRESH_TTL ? JSON.parse(JSON.stringify(app)) : app;
if ('WS' === app.call_hook?.method || if ('WS' === app.call_hook?.method ||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) { app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
const requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ; app2.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
app2.requestor = requestor; app2.notifier = app.requestor;
app2.notifier = requestor;
app2.call_hook.method = 'WS'; app2.call_hook.method = 'WS';
} }
else { else {
@@ -264,9 +245,7 @@ module.exports = function(srf, logger) {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook
// eslint-disable-next-line no-unused-vars logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
const {requestor, notifier, ...loggable} = appInfo;
logger.info({app: loggable}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
req.locals.callInfo = new CallInfo({ req.locals.callInfo = new CallInfo({
req, req,
app: app2, app: app2,
@@ -289,46 +268,40 @@ module.exports = function(srf, logger) {
const {rootSpan, siprec, application:app} = req.locals; const {rootSpan, siprec, application:app} = req.locals;
let span; let span;
try { try {
if (app.tasks && !JAMBONES_MYSQL_REFRESH_TTL) { if (app.tasks && !process.env.JAMBONES_MYSQL_REFRESH_TTL) {
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata)); app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
if (0 === app.tasks.length) throw new Error('no application provided'); if (0 === app.tasks.length) throw new Error('no application provided');
return next(); return next();
} }
/* retrieve the application to execute for this inbound call */ /* retrieve the application to execute for this inbound call */
let json; const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
if (app.app_json) { req.locals.callInfo,
json = JSON.parse(app.app_json); {service_provider_sid: req.locals.service_provider_sid},
} else { {
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {}, defaults: {
req.locals.callInfo, synthesizer: {
{ service_provider_sid: req.locals.service_provider_sid }, vendor: app.speech_synthesis_vendor,
{ language: app.speech_synthesis_language,
defaults: { voice: app.speech_synthesis_voice
synthesizer: { },
vendor: app.speech_synthesis_vendor, recognizer: {
language: app.speech_synthesis_language, vendor: app.speech_recognizer_vendor,
voice: app.speech_synthesis_voice language: app.speech_recognizer_language
},
recognizer: {
vendor: app.speech_recognizer_vendor,
language: app.speech_recognizer_language
}
} }
}); }
logger.debug({ params }, 'sending initial webhook'); });
const obj = rootSpan.startChildSpan('performAppWebhook'); logger.debug({params}, 'sending initial webhook');
span = obj.span; const obj = rootSpan.startChildSpan('performAppWebhook');
const b3 = rootSpan.getTracingPropagation(); span = obj.span;
const httpHeaders = b3 && { b3 }; const b3 = rootSpan.getTracingPropagation();
json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders); const httpHeaders = b3 && {b3};
} const json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata)); app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
span?.setAttributes({ span.setAttributes({
'http.statusCode': 200, 'http.statusCode': 200,
'app.tasks': listTaskNames(app.tasks) 'app.tasks': listTaskNames(app.tasks)
}); });
span?.end(); span.end();
if (0 === app.tasks.length) throw new Error('no application provided'); if (0 === app.tasks.length) throw new Error('no application provided');
if (siprec) { if (siprec) {

View File

@@ -1,7 +1,6 @@
const {CallDirection, CallStatus} = require('../utils/constants'); const {CallDirection, CallStatus} = require('../utils/constants');
const parseUri = require('drachtio-srf').parseUri; const parseUri = require('drachtio-srf').parseUri;
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const {JAMBONES_API_BASE_URL} = require('../config');
/** /**
* @classdesc Represents the common information for all calls * @classdesc Represents the common information for all calls
* that is provided in call status webhooks * that is provided in call status webhooks
@@ -34,23 +33,6 @@ class CallInfo {
this.callStatus = CallStatus.Trying; this.callStatus = CallStatus.Trying;
this.originatingSipIp = req.get('X-Forwarded-For'); this.originatingSipIp = req.get('X-Forwarded-For');
this.originatingSipTrunkName = req.get('X-Originating-Carrier'); 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) { else if (opts.parentCallInfo) {
// outbound call that is a child of an existing call // outbound call that is a child of an existing call
@@ -147,8 +129,8 @@ class CallInfo {
Object.assign(obj, {customerData: this._customerData}); Object.assign(obj, {customerData: this._customerData});
} }
if (JAMBONES_API_BASE_URL) { if (process.env.JAMBONES_API_BASE_URL) {
Object.assign(obj, {apiBaseUrl: JAMBONES_API_BASE_URL}); Object.assign(obj, {apiBaseUrl: process.env.JAMBONES_API_BASE_URL});
} }
if (this.publicIp) { if (this.publicIp) {
Object.assign(obj, {fsPublicIp: this.publicIp}); Object.assign(obj, {fsPublicIp: this.publicIp});

View File

@@ -13,17 +13,10 @@ const moment = require('moment');
const assert = require('assert'); const assert = require('assert');
const sessionTracker = require('./session-tracker'); const sessionTracker = require('./session-tracker');
const makeTask = require('../tasks/make_task'); const makeTask = require('../tasks/make_task');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const listTaskNames = require('../utils/summarize-tasks'); const listTaskNames = require('../utils/summarize-tasks');
const HttpRequestor = require('../utils/http-requestor'); const HttpRequestor = require('../utils/http-requestor');
const WsRequestor = require('../utils/ws-requestor'); const WsRequestor = require('../utils/ws-requestor');
const {
JAMBONES_INJECT_CONTENT,
AWS_REGION,
JAMBONZ_RECORD_WS_BASE_URL,
JAMBONZ_RECORD_WS_USERNAME,
JAMBONZ_RECORD_WS_PASSWORD,
} = require('../config');
const BADPRECONDITIONS = 'preconditions not met'; const BADPRECONDITIONS = 'preconditions not met';
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction'; const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
@@ -67,20 +60,9 @@ class CallSession extends Emitter {
this.notifiedComplete = false; this.notifiedComplete = false;
this.rootSpan = rootSpan; this.rootSpan = rootSpan;
this._origRecognizerSettings = {
vendor: this.application?.speech_recognizer_vendor,
language: this.application?.speech_recognizer_language,
};
this._origSynthesizerSettings = {
vendor: this.application?.speech_synthesis_vendor,
language: this.application?.speech_synthesis_language,
voice: this.application?.speech_synthesis_voice,
};
assert(rootSpan); assert(rootSpan);
this._recordState = RecordState.RecordingOff; this._recordState = RecordState.RecordingOff;
this._notifyEvents = false;
this.tmpFiles = new Set(); this.tmpFiles = new Set();
@@ -99,18 +81,12 @@ class CallSession extends Emitter {
this._pool = srf.locals.dbHelpers.pool; this._pool = srf.locals.dbHelpers.pool;
const handover = (newRequestor) => {
this.logger.info(`handover to new base url ${newRequestor.url}`);
this.requestor.removeAllListeners();
this.application.requestor = newRequestor;
this.requestor.on('command', this._onCommand.bind(this));
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this));
};
this.requestor.on('command', this._onCommand.bind(this)); this.requestor.on('command', this._onCommand.bind(this));
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this)); this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this)); this.requestor.on('handover', (newRequestor) => {
this.logger.info(`handover to new base url ${newRequestor.url}`);
this.application.requestor = newRequestor;
});
} }
/** /**
@@ -131,15 +107,6 @@ class CallSession extends Emitter {
return this.callInfo.applicationSid; return this.callInfo.applicationSid;
} }
get callStatus() {
return this.callInfo.callStatus;
}
get isBackGroundListen() {
return !(this.backgroundListenTask === null ||
this.backgroundListenTask === undefined);
}
/** /**
* SIP call-id for the call * SIP call-id for the call
*/ */
@@ -292,19 +259,12 @@ class CallSession extends Emitter {
return this.backgroundGatherTask; return this.backgroundGatherTask;
} }
get isListenEnabled() {
return this.backgroundListenTask;
}
get b3() { get b3() {
return this.rootSpan?.getTracingPropagation(); return this.rootSpan?.getTracingPropagation();
} }
get recordState() { return this._recordState; } get recordState() { return this._recordState; }
get notifyEvents() { return this._notifyEvents; }
set notifyEvents(notify) { this._notifyEvents = !!notify; }
set globalSttHints({hints, hintsBoost}) { set globalSttHints({hints, hintsBoost}) {
this._globalSttHints = {hints, hintsBoost}; this._globalSttHints = {hints, hintsBoost};
} }
@@ -342,22 +302,6 @@ class CallSession extends Emitter {
return this._globalSttPunctuation !== undefined; return this._globalSttPunctuation !== undefined;
} }
resetRecognizer() {
this._globalSttHints = undefined;
this._globalSttPunctuation = undefined;
this._globalAltLanguages = undefined;
this.isContinuousAsr = false;
this.asrDtmfTerminationDigits = undefined;
this.speechRecognizerLanguage = this._origRecognizerSettings.language;
this.speechRecognizerVendor = this._origRecognizerSettings.vendor;
}
resetSynthesizer() {
this.speechSynthesisLanguage = this._origSynthesizerSettings.language;
this.speechSynthesisVendor = this._origSynthesizerSettings.vendor;
this.speechSynthesisVoice = this._origSynthesizerSettings.voice;
}
async notifyRecordOptions(opts) { async notifyRecordOptions(opts) {
const {action} = opts; const {action} = opts;
this.logger.debug({opts}, 'CallSession:notifyRecordOptions'); this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
@@ -503,91 +447,28 @@ class CallSession extends Emitter {
} }
} }
async startBackgroundListen(opts, bugname) {
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]);
this.backgroundListenTask.bugname = bugname;
const resources = await this._evaluatePreconditions(this.backgroundListenTask);
const {span, ctx} = this.rootSpan.startChildSpan(`background-listen:${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() {
this.logger.debug('CallSession:stopBackgroundListen');
try {
if (this.backgroundListenTask) {
this.backgroundListenTask.removeAllListeners();
this.backgroundListenTask.kill().catch(() => {});
}
} catch (err) {
this.logger.info({err}, 'CallSession:stopBackgroundListen - Error stopping listen task');
}
}
async enableBotMode(gather, autoEnable) { async enableBotMode(gather, autoEnable) {
try { try {
const t = normalizeJambones(this.logger, [gather]); const t = normalizeJambones(this.logger, [gather]);
const task = makeTask(this.logger, t[0]); this.backgroundGatherTask = 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;
await this.disableBotMode();
}
}
this.backgroundGatherTask = task;
this._bargeInEnabled = true; this._bargeInEnabled = true;
this.backgroundGatherTask this.backgroundGatherTask
.once('dtmf', this._clearTasks.bind(this, this.backgroundGatherTask)) .once('dtmf', this._clearTasks.bind(this))
.once('vad', this._clearTasks.bind(this, this.backgroundGatherTask)) .once('vad', this._clearTasks.bind(this))
.once('transcription', this._clearTasks.bind(this, this.backgroundGatherTask)) .once('transcription', this._clearTasks.bind(this))
.once('timeout', this._clearTasks.bind(this, this.backgroundGatherTask)); .once('timeout', this._clearTasks.bind(this));
this.logger.info({gather}, 'CallSession:enableBotMode - starting background gather'); this.logger.info({gather}, 'CallSession:enableBotMode - starting background gather');
const resources = await this._evaluatePreconditions(this.backgroundGatherTask); const resources = await this._evaluatePreconditions(this.backgroundGatherTask);
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`); const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`);
this.backgroundGatherTask.span = span; this.backgroundGatherTask.span = span;
this.backgroundGatherTask.ctx = ctx; this.backgroundGatherTask.ctx = ctx;
this.backgroundGatherTask.sticky = autoEnable;
this.backgroundGatherTask.exec(this, resources) this.backgroundGatherTask.exec(this, resources)
.then(() => { .then(() => {
this.logger.info('CallSession:enableBotMode: gather completed'); this.logger.info('CallSession:enableBotMode: gather completed');
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners(); this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
this.backgroundGatherTask && this.backgroundGatherTask.span.end(); this.backgroundGatherTask && this.backgroundGatherTask.span.end();
const sticky = this.backgroundGatherTask?.sticky;
this.backgroundGatherTask = null; this.backgroundGatherTask = null;
if (sticky && !this.callGone && !this._stopping && this._bargeInEnabled) { if (autoEnable && !this.callGone && !this._stopping && this._bargeInEnabled) {
this.logger.info('CallSession:enableBotMode: restarting background gather'); this.logger.info('CallSession:enableBotMode: restarting background gather');
setImmediate(() => this.enableBotMode(gather, true)); setImmediate(() => this.enableBotMode(gather, true));
} }
@@ -603,12 +484,12 @@ class CallSession extends Emitter {
this.logger.info({err, gather}, 'CallSession:enableBotMode - Error creating gather task'); this.logger.info({err, gather}, 'CallSession:enableBotMode - Error creating gather task');
} }
} }
async disableBotMode() { disableBotMode() {
this._bargeInEnabled = false; this._bargeInEnabled = false;
if (this.backgroundGatherTask) { if (this.backgroundGatherTask) {
try { try {
this.backgroundGatherTask.removeAllListeners(); this.backgroundGatherTask.removeAllListeners();
await this.backgroundGatherTask.kill(); this.backgroundGatherTask.kill().catch((err) => {});
} catch (err) {} } catch (err) {}
this.backgroundGatherTask = null; this.backgroundGatherTask = null;
} }
@@ -665,7 +546,7 @@ class CallSession extends Emitter {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
accessKeyId: credential.access_key_id, accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key, secretAccessKey: credential.secret_access_key,
region: credential.aws_region || AWS_REGION region: credential.aws_region || process.env.AWS_REGION
}; };
} }
else if ('microsoft' === vendor) { else if ('microsoft' === vendor) {
@@ -685,50 +566,6 @@ class CallSession extends Emitter {
api_key: credential.api_key api_key: credential.api_key
}; };
} }
else if ('nuance' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
client_id: credential.client_id,
secret: credential.secret,
nuance_tts_uri: credential.nuance_tts_uri,
nuance_stt_uri: credential.nuance_stt_uri
};
}
else if ('deepgram' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key
};
}
else if ('soniox' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key
};
}
else if ('ibm' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
tts_api_key: credential.tts_api_key,
tts_region: credential.tts_region,
stt_api_key: credential.stt_api_key,
stt_region: credential.stt_region
};
}
else if ('nvidia' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
riva_server_uri: credential.riva_server_uri
};
}
else if (vendor.startsWith('custom:')) {
return {
speech_credential_sid: credential.speech_credential_sid,
auth_token: credential.auth_token,
custom_stt_url: credential.custom_stt_url,
custom_tts_url: credential.custom_tts_url
};
}
} }
else { else {
writeAlerts({ writeAlerts({
@@ -753,24 +590,22 @@ class CallSession extends Emitter {
const stackNum = this.stackIdx; const stackNum = this.stackIdx;
const task = this.tasks.shift(); const task = this.tasks.shift();
this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`);
this._notifyTaskStatus(task, {event: 'starting'});
try { try {
const resources = await this._evaluatePreconditions(task); const resources = await this._evaluatePreconditions(task);
let skip = false; let skip = false;
this.currentTask = task; this.currentTask = task;
if (TaskName.Gather === task.name && this.isBotModeEnabled) { if (TaskName.Gather === task.name && this.isBotModeEnabled) {
if (this.backgroundGatherTask.updateTaskInProgress(task) !== false) { if (this.backgroundGatherTask.updateTaskInProgress(task)) {
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
skip = true; skip = true;
} }
else { else {
this.logger.info('CallSession:exec disabling bot mode to start gather with new options'); this.logger.info('CallSession:exec disabling bot mode to start gather with new options');
await this.disableBotMode(); this.disableBotMode();
} }
} }
if (!skip) { if (!skip) {
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`); const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
span.setAttributes({'verb.summary': task.summary});
task.span = span; task.span = span;
task.ctx = ctx; task.ctx = ctx;
await task.exec(this, resources); await task.exec(this, resources);
@@ -778,7 +613,6 @@ class CallSession extends Emitter {
} }
this.currentTask = null; this.currentTask = null;
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
this._notifyTaskStatus(task, {event: 'finished'});
} catch (err) { } catch (err) {
task.span?.end(); task.span?.end();
this.currentTask = null; this.currentTask = null;
@@ -791,23 +625,20 @@ class CallSession extends Emitter {
} }
} }
if (0 === this.tasks.length && this.requestor instanceof WsRequestor && !this.callGone) { if (0 === this.tasks.length && this.hasStableDialog && this.requestor instanceof WsRequestor) {
//let span; let span;
try { try {
//const {span} = this.rootSpan.startChildSpan('waiting for commands'); const {span} = this.rootSpan.startChildSpan('waiting for commands');
//const {reason, queue, command} = await this._awaitCommandsOrHangup(); const {reason, queue, command} = await this._awaitCommandsOrHangup();
/*
span.setAttributes({ span.setAttributes({
'completion.reason': reason, 'completion.reason': reason,
'async.request.queue': queue, 'async.request.queue': queue,
'async.request.command': command 'async.request.command': command
}); });
span.end(); span.end();
*/ if (!this.hasStableDialog || this.callGone) break;
await this._awaitCommandsOrHangup();
if (this.callGone) break;
} catch (err) { } catch (err) {
//span.end(); span.end();
this.logger.info(err, 'CallSession:exec - error waiting for new commands'); this.logger.info(err, 'CallSession:exec - error waiting for new commands');
break; break;
} }
@@ -827,6 +658,7 @@ class CallSession extends Emitter {
trackTmpFile(path) { trackTmpFile(path) {
// TODO: don't add if its already in the list (should we make it a set?) // TODO: don't add if its already in the list (should we make it a set?)
this.logger.debug(`adding tmp file to track ${path}`);
this.tmpFiles.add(path); this.tmpFiles.add(path);
} }
@@ -1163,32 +995,14 @@ class CallSession extends Emitter {
} }
} }
kill(onBackgroundGatherBargein = false) { kill() {
if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)'); if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)');
else this.logger.info('CallSession:kill'); else this.logger.info('CallSession:kill');
if (this.currentTask) { if (this.currentTask) {
this.currentTask.kill(this); this.currentTask.kill(this);
this.currentTask = null; this.currentTask = null;
} }
if (onBackgroundGatherBargein) { this.tasks = [];
/* search for a config with bargein disabled */
while (this.tasks.length) {
const t = this.tasks[0];
if (t.name === TaskName.Config && t.bargeIn?.enable === false) {
/* found it, clear to that point and remove the disable
because we likely already received a partial transcription
and we don't want to kill the background gather before we
get the full transcription.
*/
delete t.bargeIn.enable;
this._bargeInEnabled = false;
this.logger.info('CallSession:kill - found bargein disabled in the stack, clearing to that point');
break;
}
this.tasks.shift();
}
}
else this.tasks = [];
this.taskIdx = 0; this.taskIdx = 0;
} }
@@ -1201,14 +1015,14 @@ class CallSession extends Emitter {
_injectTasks(newTasks) { _injectTasks(newTasks) {
const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather); const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather);
const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather; const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather;
/*
this.logger.debug({ this.logger.debug({
currentTaskList: listTaskNames(this.tasks), currentTaskList: listTaskNames(this.tasks),
newContent: listTaskNames(newTasks), newContent: listTaskNames(newTasks),
currentlyExecutingGather, currentlyExecutingGather,
gatherPos gatherPos
}, 'CallSession:_injectTasks - starting'); }, 'CallSession:_injectTasks - starting');
*/
const killGather = () => { const killGather = () => {
this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content'); this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content');
this.currentTask.kill(this); this.currentTask.kill(this);
@@ -1217,11 +1031,10 @@ class CallSession extends Emitter {
if (-1 === gatherPos) { if (-1 === gatherPos) {
/* no gather in the stack simply append tasks */ /* no gather in the stack simply append tasks */
this.tasks.push(...newTasks); this.tasks.push(...newTasks);
/*
this.logger.debug({ this.logger.debug({
updatedTaskList: listTaskNames(this.tasks) updatedTaskList: listTaskNames(this.tasks)
}, 'CallSession:_injectTasks - completed (simple append)'); }, 'CallSession:_injectTasks - completed (simple append)');
*/
/* we do need to kill the current gather if we are executing one */ /* we do need to kill the current gather if we are executing one */
if (currentlyExecutingGather) killGather(); if (currentlyExecutingGather) killGather();
return; return;
@@ -1237,7 +1050,7 @@ class CallSession extends Emitter {
} }
_onCommand({msgid, command, call_sid, queueCommand, data}) { _onCommand({msgid, command, call_sid, queueCommand, data}) {
this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command'); this.logger.info({msgid, command, queueCommand}, 'CallSession:_onCommand - received command');
const resolution = {reason: 'received command', queue: queueCommand, command}; const resolution = {reason: 'received command', queue: queueCommand, command};
switch (command) { switch (command) {
case 'redirect': case 'redirect':
@@ -1248,11 +1061,13 @@ class CallSession extends Emitter {
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list'); this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list');
this.replaceApplication(t); this.replaceApplication(t);
} }
else if (JAMBONES_INJECT_CONTENT) { else if (process.env.JAMBONES_INJECT_CONTENT) {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks (injecting content)');
this._injectTasks(t); this._injectTasks(t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
else { else {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
this.tasks.push(...t); this.tasks.push(...t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
@@ -1296,21 +1111,19 @@ class CallSession extends Emitter {
this.logger.info(`CallSession:_onCommand - invalid command ${command}`); this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
} }
if (this.wakeupResolver) { if (this.wakeupResolver) {
//this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..'); this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..');
this.wakeupResolver(resolution); this.wakeupResolver(resolution);
this.wakeupResolver = null; this.wakeupResolver = null;
} }
/*
else { else {
const {span} = this.rootSpan.startChildSpan('async command');
const {queue, command} = resolution; const {queue, command} = resolution;
const {span} = this.rootSpan.startChildSpan(`recv cmd: ${command}`);
span.setAttributes({ span.setAttributes({
'async.request.queue': queue, 'async.request.queue': queue,
'async.request.command': command 'async.request.command': command
}); });
span.end(); span.end();
} }
*/
} }
_onWsConnectionDropped() { _onWsConnectionDropped() {
@@ -1350,10 +1163,7 @@ class CallSession extends Emitter {
} }
// we are going from an early media connection to answer // we are going from an early media connection to answer
if (this.direction === CallDirection.Inbound) { await this.propagateAnswer();
// only do this for inbound call.
await this.propagateAnswer();
}
return { return {
...resources, ...resources,
...(this.isSipRecCallSession && {ep2: this.ep2}) ...(this.isSipRecCallSession && {ep2: this.ep2})
@@ -1371,6 +1181,11 @@ class CallSession extends Emitter {
}); });
//ep.cs = this; //ep.cs = this;
this.ep = ep; this.ep = ep;
ep.set({
hangup_after_bridge: false,
park_after_bridge: true
}).catch((err) => this.logger.error({err}, 'Error setting park_after_bridge'));
this.logger.debug(`allocated endpoint ${ep.uuid}`); this.logger.debug(`allocated endpoint ${ep.uuid}`);
this.ep.on('destroy', () => { this.ep.on('destroy', () => {
@@ -1443,6 +1258,7 @@ class CallSession extends Emitter {
return; return;
} }
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp}); this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
await this.ep.set('hangup_after_bridge', false);
await this.dlg.modify(this.ep.local.sdp); await this.dlg.modify(this.ep.local.sdp);
this.logger.debug('CallSession:replaceEndpoint completed'); this.logger.debug('CallSession:replaceEndpoint completed');
@@ -1524,6 +1340,7 @@ class CallSession extends Emitter {
} }
this.dlg.on('modify', this._onReinvite.bind(this)); this.dlg.on('modify', this._onReinvite.bind(this));
this.dlg.on('refer', this._onRefer.bind(this)); this.dlg.on('refer', this._onRefer.bind(this));
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`); this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
} }
} }
@@ -1531,15 +1348,9 @@ class CallSession extends Emitter {
async _onReinvite(req, res) { async _onReinvite(req, res) {
try { try {
if (this.ep) { if (this.ep) {
if (this.isSipRecCallSession) { const newSdp = await this.ep.modify(req.body);
this.logger.info('handling reINVITE for siprec call'); res.send(200, {body: newSdp});
res.send(200, {body: this.ep.local.sdp}); this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
}
else {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
}
} }
else if (this.currentTask && this.currentTask.name === TaskName.Dial) { else if (this.currentTask && this.currentTask.name === TaskName.Dial) {
this.logger.info('handling reINVITE after media has been released'); this.logger.info('handling reINVITE after media has been released');
@@ -1585,6 +1396,7 @@ class CallSession extends Emitter {
} }
if (!this.ep) { if (!this.ep) {
this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body}); this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
await this.ep.set('hangup_after_bridge', false);
} }
return {ms: this.ms, ep: this.ep}; return {ms: this.ms, ep: this.ep};
} }
@@ -1598,7 +1410,7 @@ class CallSession extends Emitter {
const pp = this._pool.promise(); const pp = this._pool.promise();
try { try {
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: looking up account'); this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: looking up account');
const [r] = await pp.query(sqlRetrieveQueueEventHook, [this.accountSid]); const [r] = await pp.query(sqlRetrieveQueueEventHook, this.accountSid);
if (0 === r.length) { if (0 === r.length) {
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: no webhook provisioned'); this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: no webhook provisioned');
this.queueEventHookRequestor = null; this.queueEventHookRequestor = null;
@@ -1757,16 +1569,9 @@ class CallSession extends Emitter {
* @param {number} sipStatus - current sip status * @param {number} sipStatus - current sip status
* @param {number} [duration] - duration of a completed call, in seconds * @param {number} [duration] - duration of a completed call, in seconds
*/ */
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) { _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
if (this.callMoved) return; if (this.callMoved) return;
if (callStatus === CallStatus.InProgress) {
// nice, call is in progress, good time to enable record
await this.enableRecordAllCall();
} else if (callStatus == CallStatus.Completed && this.isBackGroundListen) {
await this.stopBackgroundListen();
}
/* race condition: we hang up at the same time as the caller */ /* race condition: we hang up at the same time as the caller */
if (callStatus === CallStatus.Completed) { if (callStatus === CallStatus.Completed) {
if (this.notifiedComplete) return; if (this.notifiedComplete) return;
@@ -1784,7 +1589,7 @@ class CallSession extends Emitter {
try { try {
const b3 = this.b3; const b3 = this.b3;
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
await this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON(), httpHeaders); this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON(), httpHeaders);
span.end(); span.end();
} catch (err) { } catch (err) {
span.end(); span.end();
@@ -1797,42 +1602,6 @@ class CallSession extends Emitter {
.catch((err) => this.logger.error(err, 'redis error')); .catch((err) => this.logger.error(err, 'redis error'));
} }
async enableRecordAllCall() {
if (this.accountInfo.account.record_all_calls || this.application.record_all_calls) {
const listenOpts = {
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.accountInfo.account.bucket_credential.vendor}`,
wsAuth: {
username: JAMBONZ_RECORD_WS_USERNAME,
password: JAMBONZ_RECORD_WS_PASSWORD
},
mixType : 'stereo',
passDtmf: true
};
this.logger.debug({listenOpts}, 'Record all calls: enabling listen');
await this.startBackgroundListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record');
}
}
/**
* notifyTaskError - only used when websocket connection is used instead of webhooks
*/
_notifyTaskError(obj) {
if (this.requestor instanceof WsRequestor) {
this.requestor.request('jambonz:error', '/error', obj)
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskError - Error sending'));
}
}
_notifyTaskStatus(task, evt) {
if (this.notifyEvents && this.requestor instanceof WsRequestor) {
const obj = {...evt, id: task.id, name: task.name};
this.requestor.request('verb:status', '/status', obj)
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskStatus - Error sending'));
}
}
_awaitCommandsOrHangup() { _awaitCommandsOrHangup() {
assert(!this.wakeupResolver); assert(!this.wakeupResolver);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -1841,12 +1610,11 @@ class CallSession extends Emitter {
}); });
} }
_clearTasks(backgroundGather, evt) { _clearTasks(evt) {
if (this.requestor instanceof WsRequestor && !backgroundGather.cleared) { if (this.requestor instanceof WsRequestor) {
this.logger.info({evt}, 'CallSession:_clearTasks on event from background gather'); this.logger.info({evt}, 'CallSession:_clearTasks on event from background gather');
try { try {
backgroundGather.cleared = true; this.kill();
this.kill(true);
} catch (err) {} } catch (err) {}
} }
} }

View File

@@ -21,10 +21,6 @@ class RestCallSession extends CallSession {
}); });
this.req = req; this.req = req;
this.ep = ep; this.ep = ep;
// keep restDialTask reference for closing AMD
if (tasks.length) {
this.restDialTask = tasks[0];
}
this.on('callStatusChange', this._notifyCallStatusChange.bind(this)); this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
this._notifyCallStatusChange({ this._notifyCallStatusChange({
@@ -48,10 +44,6 @@ class RestCallSession extends CallSession {
* This is invoked when the called party hangs up, in order to calculate the call duration. * This is invoked when the called party hangs up, in order to calculate the call duration.
*/ */
_callerHungup() { _callerHungup() {
if (this.restDialTask) {
this.logger.info('RestCallSession: releasing AMD');
this.restDialTask.turnOffAmd();
}
this.callInfo.callTerminationBy = 'caller'; this.callInfo.callTerminationBy = 'caller';
const duration = moment().diff(this.dlg.connectTime, 'seconds'); const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});

View File

@@ -1,7 +1,6 @@
const InboundCallSession = require('./inbound-call-session'); const InboundCallSession = require('./inbound-call-session');
const {createSipRecPayload} = require('../utils/siprec-utils'); const {createSipRecPayload} = require('../utils/siprec-utils');
const {CallStatus} = require('../utils/constants'); const {CallStatus} = require('../utils/constants');
const {parseSiprecPayload} = require('../utils/siprec-utils');
/** /**
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is * @classdesc Subclass of InboundCallSession. This represents a CallSession that is
* established for an inbound SIPREC call. * established for an inbound SIPREC call.
@@ -17,32 +16,6 @@ class SipRecCallSession extends InboundCallSession {
this.metadata = metadata; 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() { async answerSipRecCall() {
try { try {
this.ms = this.getMS(); this.ms = this.getMS();

View File

@@ -2,7 +2,7 @@ const Task = require('./task');
const Emitter = require('events'); const Emitter = require('events');
const ConfirmCallSession = require('../session/confirm-call-session'); const ConfirmCallSession = require('../session/confirm-call-session');
const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants'); const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const bent = require('bent'); const bent = require('bent');
const assert = require('assert'); const assert = require('assert');
@@ -108,18 +108,9 @@ class Conference extends Task {
async kill(cs) { async kill(cs) {
super.kill(cs); super.kill(cs);
this.logger.info(`Conference:kill ${this.confName}`); this.logger.info(`Conference:kill ${this.confName}`);
if (this._playSession) {
this._playSession.kill();
this._playSession = null;
}
this.emitter.emit('kill'); this.emitter.emit('kill');
await this._doFinalMemberCheck(cs); await this._doFinalMemberCheck(cs);
if (this.ep && this.ep.connected) { if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ;
this.ep.conn.removeAllListeners('esl::event::CUSTOM::*');
this.ep.api(`conference ${this.confName} kick ${this.memberId}`)
.catch((err) => this.logger.info({err}, 'Error kicking participant'));
}
cs.clearConferenceDetails();
this.notifyTaskDone(); this.notifyTaskDone();
} }
@@ -436,19 +427,13 @@ class Conference extends Task {
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant')); .catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
} }
if (wait_hook) {
if (this.wait_hook)
delete this.wait_hook.url;
this.wait_hook = {url: wait_hook};
}
if (hookOnly && this._playSession) { if (hookOnly && this._playSession) {
this._playSession.kill(); this._playSession.kill();
this._playSession = null; this._playSession = null;
} }
if (this.wait_hook?.url && this.conf_hold_status === 'hold') { if (wait_hook && this.conf_hold_status === 'hold') {
const {dlg} = cs; const {dlg} = cs;
this._doWaitHookWhileOnHold(cs, dlg, this.wait_hook); this._doWaitHookWhileOnHold(cs, dlg, wait_hook);
} }
else if (this.conf_hold_status !== 'hold' && this._playSession) { else if (this.conf_hold_status !== 'hold' && this._playSession) {
this._playSession.kill(); this._playSession.kill();
@@ -459,9 +444,7 @@ class Conference extends Task {
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) { async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
do { do {
try { try {
let tasks = []; const tasks = await this._playHook(cs, dlg, wait_hook);
if (wait_hook.url)
tasks = await this._playHook(cs, dlg, wait_hook.url);
if (0 === tasks.length) break; if (0 === tasks.length) break;
} catch (err) { } catch (err) {
if (!this.killed) { if (!this.killed) {
@@ -588,10 +571,6 @@ class Conference extends Task {
*/ */
_kicked(cs, dlg) { _kicked(cs, dlg) {
this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`); this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`);
if (this._playSession) {
this._playSession.kill();
this._playSession = null;
}
this.replaceEndpointAndEnd(cs); this.replaceEndpointAndEnd(cs);
} }

View File

@@ -8,14 +8,9 @@ class TaskConfig extends Task {
'synthesizer', 'synthesizer',
'recognizer', 'recognizer',
'bargeIn', 'bargeIn',
'record', 'record'
'listen'
].forEach((k) => this[k] = this.data[k] || {}); ].forEach((k) => this[k] = this.data[k] || {});
if ('notifyEvents' in this.data) {
this.notifyEvents = !!this.data.notifyEvents;
}
if (this.bargeIn.enable) { if (this.bargeIn.enable) {
this.gatherOpts = { this.gatherOpts = {
verb: 'gather', verb: 'gather',
@@ -30,14 +25,8 @@ class TaskConfig extends Task {
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k]; if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
}); });
} }
if (this.data.reset) {
if (typeof this.data.reset === 'string') this.data.reset = [this.data.reset];
}
else this.data.reset = [];
if (this.bargeIn.sticky) this.autoEnable = true; if (this.bargeIn.sticky) this.autoEnable = true;
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ? this.preconditions = (this.bargeIn.enable || this.record?.action || this.data.amd) ?
TaskPreconditions.Endpoint : TaskPreconditions.Endpoint :
TaskPreconditions.None; TaskPreconditions.None;
} }
@@ -45,16 +34,11 @@ class TaskConfig extends Task {
get name() { return TaskName.Config; } get name() { return TaskName.Config; }
get hasSynthesizer() { return Object.keys(this.synthesizer).length; } get hasSynthesizer() { return Object.keys(this.synthesizer).length; }
get hasRecognizer() { return Object.keys(this.recognizer).length; } get hasRecognizer() { return Object.keys(this.recognizer).length; }
get hasRecording() { return Object.keys(this.record).length; }
get hasListen() { return Object.keys(this.listen).length; }
get summary() { get summary() {
const phrase = []; const phrase = [];
/* reset recognizer and/or synthesizer to default values? */
if (this.data.reset.length) phrase.push(`reset ${this.data.reset.join(',')}`);
if (this.bargeIn.enable) phrase.push('enable barge-in'); if (this.bargeIn.enable) phrase.push('enable barge-in');
if (this.hasSynthesizer) { if (this.hasSynthesizer) {
const {vendor:v, language:l, voice} = this.synthesizer; const {vendor:v, language:l, voice} = this.synthesizer;
@@ -66,23 +50,13 @@ class TaskConfig extends Task {
const s = `{${v},${l}}`; const s = `{${v},${l}}`;
phrase.push(`set recognizer${s}`); phrase.push(`set recognizer${s}`);
} }
if (this.hasRecording) phrase.push(this.record.action);
if (this.hasListen) {
phrase.push(this.listen.enable ? `listen ${this.listen.url}` : 'stop listen');
}
if (this.data.amd) phrase.push('enable amd'); if (this.data.amd) phrase.push('enable amd');
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`); return `${this.name}{${phrase.join(',')}`;
return `${this.name}{${phrase.join(',')}}`;
} }
async exec(cs, {ep} = {}) { async exec(cs, {ep} = {}) {
await super.exec(cs); await super.exec(cs);
if (this.notifyEvents) {
this.logger.debug(`turning event notification ${this.notifyEvents ? 'on' : 'off'}`);
cs.notifyEvents = !!this.data.notifyEvents;
}
if (this.data.amd) { if (this.data.amd) {
this.startAmd = cs.startAmd; this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd; this.stopAmd = cs.stopAmd;
@@ -96,11 +70,6 @@ class TaskConfig extends Task {
} }
} }
this.data.reset.forEach((k) => {
if (k === 'synthesizer') cs.resetSynthesizer();
else if (k === 'recognizer') cs.resetRecognizer();
});
if (this.hasSynthesizer) { if (this.hasSynthesizer) {
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default' cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
? this.synthesizer.vendor ? this.synthesizer.vendor
@@ -167,21 +136,11 @@ class TaskConfig extends Task {
this.logger.info({err}, 'Config: error starting recording'); this.logger.info({err}, 'Config: error starting recording');
} }
} }
if (this.hasListen) {
const {enable, ...opts} = this.listen;
if (enable) {
this.logger.debug({opts}, 'Config: enabling listen');
cs.startBackgroundListen({verb: 'listen', ...opts});
} else {
this.logger.info('Config: disabling listen');
cs.stopBackgroundListen();
}
}
} }
async kill(cs) { async kill(cs) {
super.kill(cs); super.kill(cs);
//if (this.ep && this.stopAmd) this.stopAmd(this.ep, this); if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
} }
_onAmdEvent(cs, evt) { _onAmdEvent(cs, evt) {

View File

@@ -16,7 +16,6 @@ class TaskDequeue extends Task {
this.queueName = this.data.name; this.queueName = this.data.name;
this.timeout = this.data.timeout || 0; this.timeout = this.data.timeout || 0;
this.beep = this.data.beep === true; this.beep = this.data.beep === true;
this.callSid = this.data.callSid;
this.emitter = new Emitter(); this.emitter = new Emitter();
this.state = DequeueResults.Timeout; this.state = DequeueResults.Timeout;
@@ -54,7 +53,7 @@ class TaskDequeue extends Task {
} }
_getMemberFromQueue(cs) { _getMemberFromQueue(cs) {
const {retrieveFromSortedSet, retrieveByPatternSortedSet} = cs.srf.locals.dbHelpers; const {popFront} = cs.srf.locals.dbHelpers;
return new Promise(async(resolve) => { return new Promise(async(resolve) => {
let timer; let timer;
@@ -71,12 +70,7 @@ class TaskDequeue extends Task {
do { do {
try { try {
let url; const url = await popFront(this.queueName);
if (this.callSid) {
url = await retrieveByPatternSortedSet(this.queueName, `*${this.callSid}`);
} else {
url = await retrieveFromSortedSet(this.queueName);
}
if (url) { if (url) {
found = true; found = true;
clearTimeout(timer); clearTimeout(timer);
@@ -84,7 +78,7 @@ class TaskDequeue extends Task {
resolve(url); resolve(url);
} }
} catch (err) { } catch (err) {
this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error Sorted Set'); this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error popFront');
} }
await sleepFor(5000); await sleepFor(5000);
} while (!this.killed && !timedout && !found); } while (!this.killed && !timedout && !found);

View File

@@ -15,7 +15,6 @@ const DtmfCollector = require('../utils/dtmf-collector');
const dbUtils = require('../utils/db-utils'); const dbUtils = require('../utils/db-utils');
const debug = require('debug')('jambonz:feature-server'); const debug = require('debug')('jambonz:feature-server');
const {parseUri} = require('drachtio-srf'); const {parseUri} = require('drachtio-srf');
const {ANCHOR_MEDIA_ALWAYS} = require('../config');
function parseDtmfOptions(logger, dtmfCapture) { function parseDtmfOptions(logger, dtmfCapture) {
let parentDtmfCollector, childDtmfCollector; let parentDtmfCollector, childDtmfCollector;
@@ -85,7 +84,6 @@ class TaskDial extends Task {
this.earlyMedia = this.data.answerOnBridge === true; this.earlyMedia = this.data.answerOnBridge === true;
this.callerId = this.data.callerId; this.callerId = this.data.callerId;
this.callerName = this.data.callerName;
this.dialMusic = this.data.dialMusic; this.dialMusic = this.data.dialMusic;
this.headers = this.data.headers || {}; this.headers = this.data.headers || {};
this.method = this.data.method || 'POST'; this.method = this.data.method || 'POST';
@@ -136,14 +134,10 @@ class TaskDial extends Task {
get name() { return TaskName.Dial; } get name() { return TaskName.Dial; }
get canReleaseMedia() { get canReleaseMedia() {
const keepAnchor = this.data.anchorMedia || return !process.env.ANCHOR_MEDIA_ALWAYS &&
this.cs.isBackGroundListen || !this.listenTask &&
ANCHOR_MEDIA_ALWAYS || !this.transcribeTask &&
this.listenTask || !this.startAmd;
this.transcribeTask ||
this.startAmd;
return !keepAnchor;
} }
get summary() { get summary() {
@@ -167,16 +161,6 @@ class TaskDial extends Task {
async exec(cs) { async exec(cs) {
await super.exec(cs); await super.exec(cs);
try { try {
if (this.listenTask) {
const {span, ctx} = this.startChildSpan(`nested:${this.listenTask.summary}`);
this.listenTask.span = span;
this.listenTask.ctx = ctx;
}
if (this.transcribeTask) {
const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`);
this.transcribeTask.span = span;
this.transcribeTask.ctx = ctx;
}
if (this.data.amd) { if (this.data.amd) {
this.startAmd = cs.startAmd; this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd; this.stopAmd = cs.stopAmd;
@@ -234,12 +218,10 @@ class TaskDial extends Task {
if (this.callSid) sessionTracker.remove(this.callSid); if (this.callSid) sessionTracker.remove(this.callSid);
if (this.listenTask) { if (this.listenTask) {
await this.listenTask.kill(cs); await this.listenTask.kill(cs);
this.listenTask.span.end();
this.listenTask = null; this.listenTask = null;
} }
if (this.transcribeTask) { if (this.transcribeTask) {
await this.transcribeTask.kill(cs); await this.transcribeTask.kill(cs);
this.transcribeTask.span.end();
this.transcribeTask = null; this.transcribeTask = null;
} }
this.notifyTaskDone(); this.notifyTaskDone();
@@ -412,26 +394,20 @@ class TaskDial extends Task {
const {req, srf} = cs; const {req, srf} = cs;
const {getSBC} = srf.locals; const {getSBC} = srf.locals;
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers; const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
const {lookupCarrier, lookupCarrierByPhoneNumber} = dbUtils(this.logger, cs.srf); const {lookupCarrier} = dbUtils(this.logger, cs.srf);
const sbcAddress = this.proxy || getSBC(); const sbcAddress = this.proxy || getSBC();
const teamsInfo = {}; const teamsInfo = {};
let fqdn; let fqdn;
if (!sbcAddress) throw new Error('no SBC found for outbound call'); if (!sbcAddress) throw new Error('no SBC found for outbound call');
this.headers = {
'X-Account-Sid': cs.accountSid,
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
...(req && req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
...(req && req.has('X-Voip-Carrier-Sid') && {'X-Voip-Carrier-Sid': req.get('X-Voip-Carrier-Sid')}),
// Put headers at the end to make sure opt.headers override all default behavior.
...this.headers
};
const opts = { const opts = {
headers: this.headers, headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers,
proxy: `sip:${sbcAddress}`, proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber, callingNumber: this.callerId || req.callingNumber
...(this.callerName && {callingName: this.callerName}) };
opts.headers = {
...opts.headers,
'X-Account-Sid': cs.accountSid
}; };
const t = this.target.find((t) => t.type === 'teams'); const t = this.target.find((t) => t.type === 'teams');
@@ -473,22 +449,7 @@ class TaskDial extends Task {
} }
if (t.type === 'phone' && t.trunk) { if (t.type === 'phone' && t.trunk) {
const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk); const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk);
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk}`); this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk})`);
if (voip_carrier_sid) {
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
}
}
/**
* trunk isn't specified,
* check if number matches any existing numbers
* */
if (t.type === 'phone' && !t.trunk) {
const str = this.callerId || req.callingNumber || '';
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
this.logger.info(
`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested phone number: ${callingNumber}`);
if (voip_carrier_sid) { if (voip_carrier_sid) {
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid; opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
} }
@@ -669,7 +630,7 @@ class TaskDial extends Task {
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg); if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg); if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep}); if (this.transcribeTask) this.transcribeTask.exec(cs, {ep2: this.epOther, ep:this.ep});
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther}); if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther});
if (this.startAmd) { if (this.startAmd) {
try { try {

View File

@@ -3,7 +3,7 @@ const {TaskName, TaskPreconditions} = require('../../utils/constants');
const Intent = require('./intent'); const Intent = require('./intent');
const DigitBuffer = require('./digit-buffer'); const DigitBuffer = require('./digit-buffer');
const Transcription = require('./transcription'); const Transcription = require('./transcription');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../../utils/normalize-jambones');
class Dialogflow extends Task { class Dialogflow extends Task {
constructor(logger, opts) { constructor(logger, opts) {
@@ -222,7 +222,6 @@ class Dialogflow extends Task {
try { try {
const obj = { const obj = {
account_sid: cs.accountSid,
text: intent.fulfillmentText, text: intent.fulfillmentText,
vendor: this.vendor, vendor: this.vendor,
language: this.language, language: this.language,

View File

@@ -1,7 +1,7 @@
const Task = require('./task'); const Task = require('./task');
const Emitter = require('events'); const Emitter = require('events');
const ConfirmCallSession = require('../session/confirm-call-session'); const ConfirmCallSession = require('../session/confirm-call-session');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants'); const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants');
const bent = require('bent'); const bent = require('bent');
@@ -18,7 +18,6 @@ class TaskEnqueue extends Task {
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.queueName = this.data.name; this.queueName = this.data.name;
this.priority = this.data.priority;
this.waitHook = this.data.waitHook; this.waitHook = this.data.waitHook;
this.emitter = new Emitter(); this.emitter = new Emitter();
@@ -71,22 +70,12 @@ class TaskEnqueue extends Task {
} }
async _addToQueue(cs, dlg) { async _addToQueue(cs, dlg) {
const {addToSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers; const {pushBack} = cs.srf.locals.dbHelpers;
const url = getUrl(cs); const url = getUrl(cs);
this.waitStartTime = Date.now(); this.waitStartTime = Date.now();
this.logger.debug({queue: this.queueName, url}, 'pushing url onto queue'); this.logger.debug({queue: this.queueName, url}, 'pushing url onto queue');
if (this.priority < 0) { const members = await pushBack(this.queueName, url);
this.logger.warn(`priority ${this.priority} is invalid, need to be non-negative integer, this.logger.info(`TaskEnqueue:_addToQueue: added to queue, length now ${members}`);
999 will be used for priority`);
}
let members = await addToSortedSet(this.queueName, url, this.priority);
if (members === 1) {
this.logger.info('TaskEnqueue:_addToQueue: added to queue');
} else {
this.logger.info('TaskEnqueue:_addToQueue: failed to add to queue');
}
members = await sortedSetLength(this.queueName);
this.notifyUrl = url; this.notifyUrl = url;
/* invoke account-level webhook for queue event notifications */ /* invoke account-level webhook for queue event notifications */
@@ -101,9 +90,9 @@ class TaskEnqueue extends Task {
} }
async _removeFromQueue(cs) { async _removeFromQueue(cs) {
const {retrieveByPatternSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers; const {removeFromList, lengthOfList} = cs.srf.locals.dbHelpers;
await retrieveByPatternSortedSet(this.queueName, `*${getUrl(cs)}`); await removeFromList(this.queueName, getUrl(cs));
return await sortedSetLength(this.queueName); return await lengthOfList(this.queueName);
} }
async performAction() { async performAction() {
@@ -290,13 +279,13 @@ class TaskEnqueue extends Task {
this.emitter.emit('dequeue', opts); this.emitter.emit('dequeue', opts);
try { try {
const {sortedSetLength} = cs.srf.locals.dbHelpers; const {lengthOfList} = cs.srf.locals.dbHelpers;
const members = await sortedSetLength(this.queueName); const members = await lengthOfList(this.queueName);
this.dequeued = true; this.dequeued = true;
cs.performQueueWebhook({ cs.performQueueWebhook({
event: 'leave', event: 'leave',
queue: this.data.name, queue: this.data.name,
length: Math.max(members, 0), length: Math.max(members - 1, 0),
leaveReason: 'dequeued', leaveReason: 'dequeued',
leaveTime: Date.now(), leaveTime: Date.now(),
dequeuer: opts.dequeuer dequeuer: opts.dequeuer
@@ -312,7 +301,7 @@ class TaskEnqueue extends Task {
} }
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) { async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) {
const {sortedSetLength, sortedSetPositionByPattern} = cs.srf.locals.dbHelpers; const {lengthOfList, getListPosition} = cs.srf.locals.dbHelpers;
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
@@ -324,14 +313,9 @@ class TaskEnqueue extends Task {
queueTime: getElapsedTime(this.waitStartTime) queueTime: getElapsedTime(this.waitStartTime)
}; };
try { try {
const queueSize = await sortedSetLength(this.queueName); const queueSize = await lengthOfList(this.queueName);
const queuePosition = await sortedSetPositionByPattern(this.queueName, `*${this.notifyUrl}`); const queuePosition = await getListPosition(this.queueName, this.notifyUrl);
Object.assign(params, { Object.assign(params, {queueSize, queuePosition});
queueSize,
queuePosition: queuePosition.length ? queuePosition[0] : 0,
callSid: this.cs.callSid,
callId: this.cs.callId,
});
} catch (err) { } catch (err) {
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`); this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
} }

View File

@@ -3,31 +3,25 @@ const {
TaskName, TaskName,
TaskPreconditions, TaskPreconditions,
GoogleTranscriptionEvents, GoogleTranscriptionEvents,
NuanceTranscriptionEvents,
AwsTranscriptionEvents, AwsTranscriptionEvents,
AzureTranscriptionEvents, AzureTranscriptionEvents
DeepgramTranscriptionEvents,
SonioxTranscriptionEvents,
IbmTranscriptionEvents,
NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('../utils/constants'); } = require('../utils/constants');
const {
JAMBONES_GATHER_EARLY_HINTS_MATCH,
JAMBONZ_GATHER_EARLY_HINTS_MATCH,
JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS,
} = require('../config');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const assert = require('assert'); const assert = require('assert');
//const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7);
const compileTranscripts = (logger, evt, arr) => { const compileTranscripts = (logger, evt, arr) => {
//logger.debug({arr, evt}, 'compile transcripts');
if (!Array.isArray(arr) || arr.length === 0) return; if (!Array.isArray(arr) || arr.length === 0) return;
let t = ''; let t = '';
for (const a of arr) { for (const a of arr) {
//logger.debug(`adding ${a.alternatives[0].transcript}`);
t += ` ${a.alternatives[0].transcript}`; t += ` ${a.alternatives[0].transcript}`;
} }
t += ` ${evt.alternatives[0].transcript}`; t += ` ${evt.alternatives[0].transcript}`;
evt.alternatives[0].transcript = t.trim(); evt.alternatives[0].transcript = t.trim();
//logger.debug(`compiled transcript: ${evt.alternatives[0].transcript}`);
}; };
class TaskGather extends Task { class TaskGather extends Task {
@@ -35,20 +29,8 @@ class TaskGather extends Task {
super(logger, opts); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
const {
setChannelVarsForStt,
normalizeTranscription,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
} = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription;
this.removeSpeechListeners = removeSpeechListeners;
this.compileSonioxTranscripts = compileSonioxTranscripts;
[ [
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
'speechTimeout', 'timeout', 'say', 'play' 'speechTimeout', 'timeout', 'say', 'play'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
@@ -58,31 +40,55 @@ class TaskGather extends Task {
/* timeout of zero means no timeout */ /* timeout of zero means no timeout */
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000; this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0); this.interim = !!this.partialResultHook || this.bargein;
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true; this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount || 1; this.minBargeinWordCount = this.data.minBargeinWordCount || 0;
if (this.data.recognizer) { if (this.data.recognizer) {
const recognizer = this.data.recognizer; const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor; this.vendor = recognizer.vendor;
this.language = recognizer.language; this.language = recognizer.language;
this.hints = recognizer.hints || [];
/* let credentials be supplied in the recognizer object at runtime */ this.hintsBoost = recognizer.hintsBoost;
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer); this.profanityFilter = recognizer.profanityFilter;
this.punctuation = !!recognizer.punctuation;
this.enhancedModel = !!recognizer.enhancedModel;
this.model = recognizer.model || 'command_and_search';
this.words = !!recognizer.words;
this.singleUtterance = recognizer.singleUtterance || true;
this.diarization = !!recognizer.diarization;
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
this.interactionType = recognizer.interactionType || 'unspecified';
this.naicsCode = recognizer.naicsCode || 0;
this.altLanguages = recognizer.altLanguages || [];
/* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */ /* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */
this.asrTimeout = typeof recognizer.asrTimeout === 'number' ? recognizer.asrTimeout * 1000 : 0; this.asrTimeout = typeof recognizer.asrTimeout === 'number' ? recognizer.asrTimeout * 1000 : 0;
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit; if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
this.isContinuousAsr = this.asrTimeout > 0; this.isContinuousAsr = this.asrTimeout > 0;
if (Array.isArray(this.data.recognizer.hints) && /* vad: if provided, we dont connect to recognizer until voice activity is detected */
0 == this.data.recognizer.hints.length && JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS) { const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
logger.debug('Gather: an empty hints array was supplied, so we will mask global hints'); this.vad = {enable, voiceMs, mode};
this.maskGlobalSttHints = true;
} /* aws options */
this.data.recognizer.hints = this.data.recognizer.hints || []; this.vocabularyName = recognizer.vocabularyName;
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || []; this.vocabularyFilterName = recognizer.vocabularyFilterName;
this.filterMethod = recognizer.filterMethod;
/* microsoft options */
this.outputFormat = recognizer.outputFormat || 'simple';
this.profanityOption = recognizer.profanityOption || 'raw';
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
this.azureSttEndpointId = recognizer.azureSttEndpointId;
this.azureAudioLogging = recognizer.audioLogging;
}
else {
this.hints = [];
this.altLanguages = [];
} }
else this.data.recognizer = {hints: [], altLanguages: []};
this.digitBuffer = ''; this.digitBuffer = '';
this._earlyMedia = this.data.earlyMedia === true; this._earlyMedia = this.data.earlyMedia === true;
@@ -98,21 +104,13 @@ class TaskGather extends Task {
/* buffer speech for continuous asr */ /* buffer speech for continuous asr */
this._bufferedTranscripts = []; this._bufferedTranscripts = [];
/* buffer for soniox transcripts */
this._sonioxTranscripts = [];
this.parentTask = parentTask; this.parentTask = parentTask;
this.partialTranscriptsCount = 0;
} }
get name() { return TaskName.Gather; } get name() { return TaskName.Gather; }
get needsStt() { return this.input.includes('speech'); } get needsStt() { return this.input.includes('speech'); }
get wantsSingleUtterance() {
return this.data.recognizer?.singleUtterance === true;
}
get earlyMedia() { get earlyMedia() {
return (this.sayTask && this.sayTask.earlyMedia) || return (this.sayTask && this.sayTask.earlyMedia) ||
(this.playTask && this.playTask.earlyMedia); (this.playTask && this.playTask.earlyMedia);
@@ -134,28 +132,32 @@ class TaskGather extends Task {
} }
async exec(cs, {ep}) { async exec(cs, {ep}) {
this.logger.debug({options: this.data}, 'Gather:exec'); this.logger.debug('Gather:exec');
await super.exec(cs); await super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) { if (cs.hasGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints; const {hints, hintsBoost} = cs.globalSttHints;
const setOfHints = new Set(this.data.recognizer.hints this.hints = this.hints.concat(hints);
.concat(hints) if (!this.hintsBoost && hintsBoost) this.hintsBoost = hintsBoost;
.filter((h) => typeof h === 'string' && h.length > 0)); this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
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'); 'Gather:exec - applying global sttHints');
} }
if (process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH &&
!this.isContinuousAsr &&
this.hints.length > 0 && this.hints.length <= 10) {
this.earlyHintsMatch = true;
this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled');
}
if (cs.hasAltLanguages) { if (cs.hasAltLanguages) {
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages); this.altLanguages = this.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages}, this.logger.debug({altLanguages: this.altLanguages},
'Gather:exec - applying altLanguages'); 'Gather:exec - applying altLanguages');
} }
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) { if (cs.hasGlobalSttPunctuation) {
this.data.recognizer.punctuation = cs.globalSttPunctuation; this.punctuation = cs.globalSttPunctuation;
} }
if (!this.isContinuousAsr && cs.isContinuousAsr) { if (!this.isContinuousAsr && cs.isContinuousAsr) {
this.isContinuousAsr = true; this.isContinuousAsr = true;
@@ -166,28 +168,10 @@ class TaskGather extends Task {
asrDtmfTerminationDigit: this.asrDtmfTerminationDigit asrDtmfTerminationDigit: this.asrDtmfTerminationDigit
}, 'Gather:exec - enabling continuous ASR since it is turned on for the session'); }, 'Gather:exec - enabling continuous ASR since it is turned on for the session');
} }
if ((JAMBONZ_GATHER_EARLY_HINTS_MATCH || JAMBONES_GATHER_EARLY_HINTS_MATCH) && this.needsStt &&
!this.isContinuousAsr &&
this.data.recognizer?.hints?.length > 0 && this.data.recognizer?.hints?.length <= 10) {
this.earlyHintsMatch = true;
this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled');
}
this.ep = ep; this.ep = ep;
if ('default' === this.vendor || !this.vendor) { if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
this.vendor = cs.speechRecognizerVendor; if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor; this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (this.needsStt && !this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
if (this.needsStt && !this.sttCredentials) { if (this.needsStt && !this.sttCredentials) {
const {writeAlerts, AlertType} = cs.srf.locals; const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`); this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
@@ -196,45 +180,21 @@ class TaskGather extends Task {
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor vendor: this.vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
// Notify application that STT vender is wrong.
this.notifyError({ throw new Error(`no speech-to-text service credentials for ${this.vendor} have been configured`);
msg: 'ASR error',
details: `No speech-to-text service credentials for ${this.vendor} have been configured`
});
this.notifyTaskDone();
throw new Error(`No speech-to-text service credentials for ${this.vendor} have been configured`);
} }
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
/* get nuance access token */
const {client_id, secret} = this.sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
this.logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token};
}
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
/* get ibm access token */
const {stt_api_key, stt_region} = this.sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
}
const startListening = (cs, ep) => { const startListening = (cs, ep) => {
this._startTimer(); this._startTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer(); // dont start asr timer until we have a transcription
//if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
if (this.input.includes('speech') && !this.listenDuringPrompt) { if (this.input.includes('speech') && !this.listenDuringPrompt) {
this._initSpeech(cs, ep) this._initSpeech(cs, ep)
.then(() => { .then(() => {
if (this.killed) {
this.logger.info('Gather:exec - task was quickly killed so do not transcribe');
return;
}
this._startTranscribing(ep); this._startTranscribing(ep);
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
}) })
.catch((err) => { .catch(() => {});
this.logger.error({err}, 'error in initSpeech');
});
} }
}; };
@@ -248,15 +208,7 @@ class TaskGather extends Task {
span.end(); span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing tts'); if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
this.logger.debug('Gather: nested say task completed'); this.logger.debug('Gather: nested say task completed');
if (!this.killed) { if (!this.killed) startListening(cs, ep);
startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
this.logger.debug('Gather:exec - starting transcription timers after say completes');
ep.startTranscriptionTimers((err) => {
if (err) this.logger.error({err}, 'Gather:exec - error starting transcription timers');
});
}
}
}); });
} }
else if (this.playTask) { else if (this.playTask) {
@@ -268,24 +220,10 @@ class TaskGather extends Task {
span.end(); span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing url'); if (err) this.logger.error({err}, 'Gather:exec Error playing url');
this.logger.debug('Gather: nested play task completed'); this.logger.debug('Gather: nested play task completed');
if (!this.killed) { if (!this.killed) startListening(cs, ep);
startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
this.logger.debug('Gather:exec - starting transcription timers after play completes');
ep.startTranscriptionTimers((err) => {
if (err) this.logger.error({err}, 'Gather:exec - error starting transcription timers');
});
}
}
}); });
} }
else { else startListening(cs, ep);
if (this.killed) {
this.logger.info('Gather:exec - task was immediately killed so do not transcribe');
return;
}
startListening(cs, ep);
}
if (this.input.includes('speech') && this.listenDuringPrompt) { if (this.input.includes('speech') && this.listenDuringPrompt) {
await this._initSpeech(cs, ep); await this._initSpeech(cs, ep);
@@ -302,7 +240,14 @@ class TaskGather extends Task {
} catch (err) { } catch (err) {
this.logger.error(err, 'TaskGather:exec error'); this.logger.error(err, 'TaskGather:exec error');
} }
this.removeSpeechListeners(ep); ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
ep.removeCustomEventListener(GoogleTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AwsTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
ep.removeCustomEventListener(AzureTranscriptionEvents.VadDetected);
} }
kill(cs) { kill(cs) {
@@ -310,7 +255,6 @@ class TaskGather extends Task {
this._killAudio(cs); this._killAudio(cs);
this.ep.removeAllListeners('dtmf'); this.ep.removeAllListeners('dtmf');
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearAsrTimer();
this.playTask?.span.end(); this.playTask?.span.end();
this.sayTask?.span.end(); this.sayTask?.span.end();
this._resolve('killed'); this._resolve('killed');
@@ -363,101 +307,107 @@ class TaskGather extends Task {
} }
async _initSpeech(cs, ep) { async _initSpeech(cs, ep) {
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer); const opts = {};
switch (this.vendor) {
case 'google':
this.bugname = 'google_transcribe';
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break;
case 'aws': if (this.vad?.enable) {
case 'polly': opts.START_RECOGNIZING_ON_VAD = 1;
this.bugname = 'aws_transcribe'; if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); else opts.RECOGNIZER_VAD_VOICE_MS = 125;
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep)); if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
break;
case 'microsoft':
this.bugname = 'azure_transcribe';
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected,
this._onNoSpeechDetected.bind(this, cs, ep));
ep.addCustomEventListener(AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break;
case 'nuance':
this.bugname = 'nuance_transcribe';
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
opts.NUANCE_STALL_TIMERS = 1;
}
break;
case 'deepgram':
this.bugname = 'deepgram_transcribe';
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect, this._onDeepgramConnect.bind(this, cs, ep));
ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
this._onDeepGramConnectFailure.bind(this, cs, ep));
break;
case 'soniox':
this.bugname = 'soniox_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break;
case 'ibm':
this.bugname = 'ibm_transcribe';
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(IbmTranscriptionEvents.Connect, this._onIbmConnect.bind(this, cs, ep));
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onIbmConnectFailure.bind(this, cs, ep));
break;
case 'nvidia':
this.bugname = 'nvidia_transcribe';
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* I think nvidia has this (??) - stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
opts.NVIDIA_STALL_TIMERS = 1;
}
break;
default:
if (this.vendor.startsWith('custom:')) {
this.bugname = `${this.vendor}_transcribe`;
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onJambonzConnect.bind(this, cs, ep));
ep.addCustomEventListener(JambonzTranscriptionEvents.ConnectFailure,
this._onJambonzConnectFailure.bind(this, cs, ep));
break;
}
else {
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`});
this.notifyTaskDone();
throw new Error(`Invalid vendor ${this.vendor}`);
}
} }
/* common handler for all stt engine errors */ if ('google' === this.vendor) {
ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep)); this.bugname = 'google_transcribe';
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
['singleUtterance', 'GOOGLE_SPEECH_SINGLE_UTTERANCE'],
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
else if (this[arr[0]] === false) opts[arr[1]] = false;
});
if (this.hints.length > 0) {
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
if (typeof this.hintsBoost === 'number') {
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
}
}
if (this.altLanguages.length > 0) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
else opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
if ('unspecified' !== this.interactionType) {
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
}
opts.GOOGLE_SPEECH_MODEL = this.model;
if (this.diarization && this.diarizationMinSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
}
if (this.diarization && this.diarizationMaxSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
}
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
}
else if (['aws', 'polly'].includes(this.vendor)) {
this.bugname = 'aws_transcribe';
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
if (this.vocabularyFilterName) {
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
}
if (this.sttCredentials) {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
}
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
}
else if ('microsoft' === this.vendor) {
this.bugname = 'azure_transcribe';
if (this.sttCredentials) {
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': api_key,
'AZURE_REGION': region
});
if (this.azureSttEndpointId) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
}
else if (use_custom_stt && custom_stt_endpoint) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
}
}
if (this.hints && this.hints.length > 0) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
if (this.altLanguages && this.altLanguages.length > 0) {
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
}
else {
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
}
if (this.azureAudioLogging) opts.AZURE_AUDIO_LOGGING = 1;
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
if (this.profanityOption && this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
else if (this.timeout === 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = 120000; // lengthy
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoSpeechDetected.bind(this, cs, ep));
ep.addCustomEventListener(AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
}
await ep.set(opts) await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables')); .catch((err) => this.logger.info(err, 'Error setting channel variables'));
} }
@@ -490,8 +440,7 @@ class TaskGather extends Task {
if (0 === this.timeout) return; if (0 === this.timeout) return;
this._clearTimer(); this._clearTimer();
this._timeoutTimer = setTimeout(() => { this._timeoutTimer = setTimeout(() => {
if (this.isContinuousAsr) this._startAsrTimer(); this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
else this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
}, this.timeout); }, this.timeout);
} }
@@ -557,24 +506,42 @@ class TaskGather extends Task {
// make sure this is not a transcript from answering machine detection // make sure this is not a transcript from answering machine detection
const bugname = fsEvent.getHeader('media-bugname'); const bugname = fsEvent.getHeader('media-bugname');
const finished = fsEvent.getHeader('transcription-session-finished'); const finished = fsEvent.getHeader('transcription-session-finished');
this.logger.debug({evt, bugname, finished}, `Gather:_onTranscription for vendor ${this.vendor}`);
if (bugname && this.bugname !== bugname) return; if (bugname && this.bugname !== bugname) return;
if (this.vendor === 'ibm') { if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
if (evt?.state === 'listening') return; if ('microsoft' === this.vendor) {
const final = evt.RecognitionStatus === 'Success';
if (final) {
// don't sort based on confidence: https://github.com/Azure-Samples/cognitive-services-speech-sdk/issues/1463
//const nbest = evt.NBest.sort((a, b) => b.Confidence - a.Confidence);
const nbest = evt.NBest;
const language_code = evt.PrimaryLanguage?.Language || this.language;
evt = {
is_final: true,
language_code,
alternatives: [
{
confidence: nbest[0].Confidence,
transcript: nbest[0].Display
}
]
};
}
else {
evt = {
is_final: false,
alternatives: [
{
transcript: evt.Text
}
]
};
}
} }
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language); if (this.earlyHintsMatch && evt.is_final === false) {
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 transcript = evt.alternatives[0].transcript?.toLowerCase();
const hints = this.data.recognizer?.hints || []; if (this.hints.find((h) => h.toLowerCase() === transcript)) {
if (hints.find((h) => h.toLowerCase() === transcript)) {
this.logger.debug({evt}, 'Gather:_onTranscription: early hint match'); this.logger.debug({evt}, 'Gather:_onTranscription: early hint match');
this._resolve('speech', evt); this._resolve('speech', evt);
return; return;
@@ -582,21 +549,22 @@ class TaskGather extends Task {
} }
/* count words for bargein feature */ /* count words for bargein feature */
const words = evt.alternatives[0]?.transcript.split(' ').length; const words = evt.alternatives[0].transcript.split(' ').length;
const bufferedWords = this._sonioxTranscripts.length + const bufferedWords = this._bufferedTranscripts.reduce((count, e) => {
this._bufferedTranscripts.reduce((count, e) => count + e.alternatives[0]?.transcript.split(' ').length, 0); return count + e.alternatives[0].transcript.split(' ').length;
}, 0);
if (evt.is_final) { if (evt.is_final) {
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) { if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
if (finished === 'true' && ['microsoft', 'deepgram'].includes(this.vendor)) { if ('microsoft' === this.vendor && finished === 'true') {
this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding'); this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding');
} }
else { else {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening'); this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
this._startTranscribing(ep);
} }
return; return;
} }
if (this.isContinuousAsr) { if (this.isContinuousAsr) {
/* append the transcript and start listening again for asrTimeout */ /* append the transcript and start listening again for asrTimeout */
const t = evt.alternatives[0].transcript; const t = evt.alternatives[0].transcript;
@@ -616,9 +584,7 @@ class TaskGather extends Task {
return this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout'); return this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
} }
this._startAsrTimer(); 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 { else {
if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) { if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) {
@@ -629,12 +595,6 @@ class TaskGather extends Task {
return; return;
} }
else { 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); this._resolve('speech', evt);
} }
} }
@@ -645,8 +605,6 @@ class TaskGather extends Task {
others do not. others do not.
*/ */
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD; //const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
this._clearTimer();
this._startTimer();
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) { if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) {
if (!this.playComplete) { if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech'); this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
@@ -660,13 +618,6 @@ class TaskGather extends Task {
this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt}, this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt},
this.cs.callInfo, httpHeaders)); this.cs.callInfo, httpHeaders));
} }
if (this.vendor === 'soniox') {
this._clearTimer();
if (evt.vendor.finalWords.length) {
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
this._sonioxTranscripts.push(evt.vendor.finalWords);
}
}
} }
} }
_onEndOfUtterance(cs, ep) { _onEndOfUtterance(cs, ep) {
@@ -675,102 +626,11 @@ class TaskGather extends Task {
this._killAudio(cs); 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); this._startTranscribing(ep);
} }
} }
_onStartOfSpeech(cs, ep) {
this.logger.debug('TaskGather:_onStartOfSpeech');
if (this.bargein) {
this._killAudio(cs);
}
}
_onTranscriptionComplete(cs, ep) {
this.logger.debug('TaskGather:_onTranscriptionComplete');
}
_onDeepgramConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onDeepgramConnect');
}
_onJambonzConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onJambonzConnect');
}
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskGather:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
this.logger.info({evt}, 'TaskGather:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
_onDeepGramConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onDeepgramConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
vendor: 'deepgram',
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor deepgram: ${reason}`});
this.notifyTaskDone();
}
_onJambonzConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onJambonzConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
this.notifyTaskDone();
}
_onIbmConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onIbmConnect');
}
_onIbmConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onIbmConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
vendor: 'ibm',
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor IBM: ${reason}`});
this.notifyTaskDone();
}
_onIbmError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskGather:_onIbmError'); }
_onVadDetected(cs, ep) { _onVadDetected(cs, ep) {
if (this.bargein && this.minBargeinWordCount === 0) { if (this.bargein && this.minBargeinWordCount === 0) {
this.logger.debug('TaskGather:_onVadDetected'); this.logger.debug('TaskGather:_onVadDetected');
@@ -798,10 +658,6 @@ class TaskGather extends Task {
if (this.resolved) return; if (this.resolved) return;
this.resolved = true; this.resolved = true;
// Clear dtmf event
if (this.dtmfBargein) {
this.ep.removeAllListeners('dtmf');
}
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearTimer(); this._clearTimer();
@@ -817,11 +673,7 @@ class TaskGather extends Task {
this.logger.debug({evt}, 'TaskGather:resolve buffered results'); this.logger.debug({evt}, 'TaskGather:resolve buffered results');
} }
this.span.setAttributes({ this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
channel: 1,
'stt.resolve': reason,
'stt.result': JSON.stringify(evt)
});
if (this.needsStt && this.ep && this.ep.connected) { if (this.needsStt && this.ep && this.ep.connected) {
this.ep.stopTranscription({vendor: this.vendor}) this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.error({err}, 'Error stopping transcription')); .catch((err) => this.logger.error({err}, 'Error stopping transcription'));

View File

@@ -18,7 +18,6 @@ class TaskHangup extends Task {
await super.exec(cs); await super.exec(cs);
try { try {
await dlg.destroy({headers: this.headers}); await dlg.destroy({headers: this.headers});
cs._callReleased();
} catch (err) { } catch (err) {
this.logger.error(err, 'TaskHangup:exec - Error hanging up call'); this.logger.error(err, 'TaskHangup:exec - Error hanging up call');
} }

View File

@@ -1,6 +1,6 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
class Lex extends Task { class Lex extends Task {
constructor(logger, opts) { constructor(logger, opts) {
@@ -189,7 +189,6 @@ class Lex extends Task {
this.logger.debug(`tts with ${this.vendor} ${this.voice}`); this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const {filePath, servedFromCache} = await synthAudio(stats, { const {filePath, servedFromCache} = await synthAudio(stats, {
account_sid: cs.accountSid,
text: msg, text: msg,
vendor: this.vendor, vendor: this.vendor,
language: this.language, language: this.language,

View File

@@ -2,8 +2,6 @@ const Task = require('./task');
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants'); const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const moment = require('moment'); const moment = require('moment');
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
const DTMF_SPAN_NAME = 'dtmf';
class TaskListen extends Task { class TaskListen extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
@@ -12,7 +10,7 @@ class TaskListen extends Task {
[ [
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep', 'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
'sampleRate', 'timeout', 'transcribe', 'wsAuth', 'disableBidirectionalAudio' 'sampleRate', 'timeout', 'transcribe', 'wsAuth'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
this.mixType = this.mixType || 'mono'; this.mixType = this.mixType || 'mono';
@@ -22,16 +20,12 @@ class TaskListen extends Task {
this.nested = parentTask instanceof Task; this.nested = parentTask instanceof Task;
this.results = {}; this.results = {};
this.playAudioQueue = [];
this.isPlayingAudioFromQueue = false;
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this); if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
} }
get name() { return TaskName.Listen; } get name() { return TaskName.Listen; }
set bugname(name) { this._bugname = name; }
async exec(cs, {ep}) { async exec(cs, {ep}) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
@@ -64,12 +58,10 @@ class TaskListen extends Task {
super.kill(cs); super.kill(cs);
this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`); this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`);
this._clearTimer(); this._clearTimer();
this.playAudioQueue = [];
if (this.ep && this.ep.connected) { if (this.ep && this.ep.connected) {
this.logger.debug('TaskListen:kill closing websocket'); this.logger.debug('TaskListen:kill closing websocket');
try { try {
const args = this._bugname ? [this._bugname] : []; await this.ep.forkAudioStop();
await this.ep.forkAudioStop(...args);
this.logger.debug('TaskListen:kill successfully closed websocket'); this.logger.debug('TaskListen:kill successfully closed websocket');
} catch (err) { } catch (err) {
this.logger.info(err, 'TaskListen:kill'); this.logger.info(err, 'TaskListen:kill');
@@ -89,16 +81,13 @@ class TaskListen extends Task {
async updateListen(status) { async updateListen(status) {
if (!this.killed && this.ep && this.ep.connected) { if (!this.killed && this.ep && this.ep.connected) {
const args = this._bugname ? [this._bugname] : [];
this.logger.info(`TaskListen:updateListen status ${status}`); this.logger.info(`TaskListen:updateListen status ${status}`);
switch (status) { switch (status) {
case ListenStatus.Pause: case ListenStatus.Pause:
await this.ep.forkAudioPause(...args) await this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
.catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
break; break;
case ListenStatus.Resume: case ListenStatus.Resume:
await this.ep.forkAudioResume(...args) await this.ep.forkAudioResume().catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
.catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
break; break;
} }
} }
@@ -127,7 +116,6 @@ class TaskListen extends Task {
wsUrl: this.hook.url, wsUrl: this.hook.url,
mixType: this.mixType, mixType: this.mixType,
sampling: this.sampleRate, sampling: this.sampleRate,
...(this._bugname && {bugname: this._bugname}),
metadata metadata
}); });
this.recordStartTime = moment(); this.recordStartTime = moment();
@@ -148,9 +136,7 @@ class TaskListen extends Task {
} }
/* support bi-directional audio */ /* support bi-directional audio */
if (!this.disableBiDirectionalAudio) { ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
}
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep)); ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep)); ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
} }
@@ -169,25 +155,12 @@ class TaskListen extends Task {
} }
_onDtmf(ep, evt) { _onDtmf(ep, evt) {
const {dtmf, duration} = evt; this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${evt.dtmf}`);
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${dtmf}`);
if (this.passDtmf && this.ep?.connected) { if (this.passDtmf && this.ep?.connected) {
const obj = {event: 'dtmf', dtmf, duration}; const obj = {event: 'dtmf', dtmf: evt.dtmf, duration: evt.duration};
const args = this._bugname ? [this._bugname, obj] : [obj]; this.ep.forkAudioSendText(obj)
this.ep.forkAudioSendText(...args)
.catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf')); .catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf'));
} }
/* add a child span for the dtmf event */
const msDuration = Math.floor((duration / 8000) * 1000);
const {span} = this.startChildSpan(`${DTMF_SPAN_NAME}:${dtmf}`);
span.setAttributes({
channel: 1,
dtmf,
duration: `${msDuration}ms`
});
span.end();
if (evt.dtmf === this.finishOnKey) { if (evt.dtmf === this.finishOnKey) {
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`); this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
this.results.digits = evt.dtmf; this.results.digits = evt.dtmf;
@@ -209,44 +182,16 @@ class TaskListen extends Task {
this.notifyTaskDone(); this.notifyTaskDone();
} }
async _playAudio(ep, evt, logger) {
try {
const results = await ep.play(evt.file);
logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
const obj = {
type: 'playDone',
data: {
id: evt.id,
...results
}
};
const args = this._bugname ? [this._bugname, obj] : [obj];
ep.forkAudioSendText(...args);
} catch (err) {
logger.error({err}, 'Error playing file');
}
}
async _onPlayAudio(ep, evt) { async _onPlayAudio(ep, evt) {
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`); this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
if (!evt.queuePlay) { try {
this.playAudioQueue = []; const results = await ep.play(evt.file);
this._playAudio(ep, evt, this.logger); this.logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
this.isPlayingAudioFromQueue = false; ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
return;
} }
catch (err) {
if (this.playAudioQueue.length <= MAX_PLAY_AUDIO_QUEUE_SIZE) { this.logger.error({err}, 'Error playing file');
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) { _onKillAudio(ep) {

View File

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

View File

@@ -1,8 +1,8 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const bent = require('bent'); const bent = require('bent');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const {K8S} = require('../config');
class TaskMessage extends Task { class TaskMessage extends Task {
constructor(logger, opts) { constructor(logger, opts) {
super(logger, opts); super(logger, opts);
@@ -42,7 +42,7 @@ class TaskMessage extends Task {
} }
if (gw) { if (gw) {
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message'); this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
url = K8S ? 'http://smpp' : getSmpp(); url = process.env.K8S ? 'http://smpp' : getSmpp();
relativeUrl = '/sms'; relativeUrl = '/sms';
payload = { payload = {
...payload, ...payload,

View File

@@ -22,22 +22,7 @@ class TaskPlay extends Task {
async exec(cs, {ep}) { async exec(cs, {ep}) {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
let timeout;
let playbackSeconds = 0;
let playbackMilliseconds = 0;
let completed = !(this.timeoutSecs > 0 || this.loop);
if (this.timeoutSecs > 0) {
timeout = setTimeout(async() => {
completed = true;
try {
await this.kill(cs);
} catch (err) {
this.logger.info(err, 'Error killing audio on timeoutSecs');
}
}, this.timeoutSecs * 1000);
}
try { try {
this.notifyStatus({event: 'start-playback'});
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) { while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
if (cs.isInConference) { if (cs.isInConference) {
const {memberId, confName, confUuid} = cs; const {memberId, confName, confUuid} = cs;
@@ -49,24 +34,14 @@ class TaskPlay extends Task {
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url); await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
} }
} else { } else {
let file = this.url; const file = (this.timeoutSecs >= 0 || this.seekOffset >= 0) ?
if (this.seekOffset >= 0) { {file: this.url, seekOffset: this.seekOffset, timeoutSecs: this.timeoutSecs} : this.url;
file = {file: this.url, seekOffset: this.seekOffset};
this.seekOffset = -1;
}
const result = await ep.play(file); const result = await ep.play(file);
playbackSeconds += parseInt(result.playbackSeconds); await this.performAction(Object.assign(result, {reason: 'playCompleted'}),
playbackMilliseconds += parseInt(result.playbackMilliseconds); !(this.parentTask || cs.isConfirmCallSession));
if (this.killed || !this.loop || completed) {
if (timeout) clearTimeout(timeout);
await this.performAction(
Object.assign(result, {reason: 'playCompleted', playbackSeconds, playbackMilliseconds}),
!(this.parentTask || cs.isConfirmCallSession));
}
} }
} }
} catch (err) { } catch (err) {
if (timeout) clearTimeout(timeout);
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`); this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
} }
this.emit('playDone'); this.emit('playDone');
@@ -81,8 +56,7 @@ class TaskPlay extends Task {
this.killPlayToConfMember(this.ep, memberId, confName); this.killPlayToConfMember(this.ep, memberId, confName);
} }
else { else {
this.notifyStatus({event: 'kill-playback'}); await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
} }
} }
} }

View File

@@ -1,7 +1,7 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName} = require('../utils/constants'); const {TaskName} = require('../utils/constants');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
/** /**
* Manages an outdial made via REST API * Manages an outdial made via REST API
@@ -11,7 +11,6 @@ class TaskRestDial extends Task {
super(logger, opts); super(logger, opts);
this.from = this.data.from; this.from = this.data.from;
this.callerName = this.data.callerName;
this.fromHost = this.data.fromHost; this.fromHost = this.data.fromHost;
this.to = this.data.to; this.to = this.data.to;
this.call_hook = this.data.call_hook; this.call_hook = this.data.call_hook;
@@ -23,44 +22,29 @@ class TaskRestDial extends Task {
get name() { return TaskName.RestDial; } get name() { return TaskName.RestDial; }
set appJson(app_json) {
this.app_json = app_json;
}
/** /**
* INVITE has just been sent at this point * INVITE has just been sent at this point
*/ */
async exec(cs) { async exec(cs) {
await super.exec(cs); await super.exec(cs);
this.cs = cs; this.req = cs.req;
this.canCancel = true;
if (this.data.amd) {
this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs));
}
this._setCallTimer(); this._setCallTimer();
await this.awaitTaskDone(); await this.awaitTaskDone();
} }
turnOffAmd() {
if (this.callSession.ep && this.callSession.ep.amd) this.stopAmd(this.callSession.ep, this);
}
kill(cs) { kill(cs) {
super.kill(cs); super.kill(cs);
this._clearCallTimer(); this._clearCallTimer();
if (this.canCancel) { if (this.req) {
this.canCancel = false; this.req.cancel();
cs?.req?.cancel(); this.req = null;
} }
this.notifyTaskDone(); this.notifyTaskDone();
} }
async _onConnect(dlg) { async _onConnect(dlg) {
this.canCancel = false; this.req = null;
const cs = this.callSession; const cs = this.callSession;
cs.setDialog(dlg); cs.setDialog(dlg);
@@ -81,19 +65,7 @@ class TaskRestDial extends Task {
} }
} }
}; };
if (this.startAmd) { const tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
try {
this.startAmd(this.callSession, this.callSession.ep, this, this.data.amd);
} catch (err) {
this.logger.info({err}, 'Rest:dial:Call established - Error calling startAmd');
}
}
let tasks;
if (this.app_json) {
tasks = JSON.parse(this.app_json);
} else {
tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
}
if (tasks && Array.isArray(tasks)) { if (tasks && Array.isArray(tasks)) {
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`); this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata))); cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
@@ -107,7 +79,7 @@ class TaskRestDial extends Task {
_onCallStatus(status) { _onCallStatus(status) {
this.logger.debug(`CallStatus: ${status}`); this.logger.debug(`CallStatus: ${status}`);
if (status >= 200) { if (status >= 200) {
this.canCancel = false; this.req = null;
this._clearCallTimer(); this._clearCallTimer();
if (status !== 200) this.notifyTaskDone(); if (status !== 200) this.notifyTaskDone();
} }
@@ -125,16 +97,7 @@ class TaskRestDial extends Task {
_onCallTimeout() { _onCallTimeout() {
this.logger.debug('TaskRestDial: timeout expired without answer, killing task'); this.logger.debug('TaskRestDial: timeout expired without answer, killing task');
this.timer = null; this.timer = null;
this.kill(this.cs); this.kill();
}
_onAmdEvent(cs, evt) {
this.logger.info({evt}, 'Rest:dial:_onAmdEvent');
const {actionHook} = this.data.amd;
this.performHook(cs, actionHook, evt)
.catch((err) => {
this.logger.error({err}, 'Rest:dial:_onAmdEvent - error calling actionHook');
});
} }
} }

View File

@@ -1,26 +1,96 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const pollySSMLSplit = require('polly-ssml-split');
const breakLengthyTextIfNeeded = (logger, text) => { const breakLengthyTextIfNeeded = (logger, text) => {
const chunkSize = 1000; const chunkSize = 1000;
if (text.length <= chunkSize) return [text];
const result = [];
const isSSML = text.startsWith('<speak>'); const isSSML = text.startsWith('<speak>');
if (text.length <= chunkSize || !isSSML) return [text]; let startPos = 0;
const options = { let charPos = isSSML ? 7 : 0; // skip <speak>
// MIN length let tag;
softLimit: 100, //logger.debug({isSSML}, `breakLengthyTextIfNeeded: handling text of length ${text.length}`);
// MAX length, exclude 15 characters <speak></speak> while (startPos + charPos < text.length) {
hardLimit: chunkSize - 15, if (isSSML && !tag && text[startPos + charPos] === '<') {
// Set of extra split characters (Optional property) const tagStartPos = ++charPos;
extraSplitChars: ',;!?', while (startPos + charPos < text.length) {
}; if (text[startPos + charPos] === '>') {
pollySSMLSplit.configure(options); if (text[startPos + charPos - 1] === '\\') tag = null;
try { else if (!tag) tag = text.substring(startPos + tagStartPos, startPos + charPos - 1);
return pollySSMLSplit.split(text); break;
} catch (err) { }
logger.info({err}, 'Error spliting SSML long text'); if (!tag) {
return [text]; 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++;
} }
// 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 { class TaskSay extends Task {
@@ -35,8 +105,6 @@ class TaskSay extends Task {
this.loop = this.data.loop || 1; this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia); this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
this.synthesizer = this.data.synthesizer || {}; this.synthesizer = this.data.synthesizer || {};
this.disableTtsCache = this.data.disableTtsCache;
this.options = this.synthesizer.options || {};
} }
get name() { return TaskName.Say; } get name() { return TaskName.Say; }
@@ -62,34 +130,14 @@ class TaskSay extends Task {
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ? const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
this.synthesizer.language : this.synthesizer.language :
cs.speechSynthesisLanguage ; cs.speechSynthesisLanguage ;
let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ? const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
this.synthesizer.voice : this.synthesizer.voice :
cs.speechSynthesisVoice; cs.speechSynthesisVoice;
const engine = this.synthesizer.engine || 'standard'; const engine = this.synthesizer.engine || 'standard';
const salt = cs.callSid; const salt = cs.callSid;
let credentials = cs.getSpeechCredentials(vendor, 'tts'); const credentials = cs.getSpeechCredentials(vendor, 'tts');
/* parse Nuance voices into name and model */ this.logger.info({vendor, language, voice}, 'TaskSay:exec');
let model;
if (vendor === 'nuance' && voice) {
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
if (arr) {
voice = arr[1];
model = arr[2];
}
}
/* allow for microsoft custom region voice and api_key to be specified as an override */
if (vendor === 'microsoft' && this.options.deploymentId) {
credentials = credentials || {};
credentials.use_custom_tts = true;
credentials.custom_tts_endpoint = this.options.deploymentId;
credentials.api_key = this.options.apiKey || credentials.apiKey;
credentials.region = this.options.region || credentials.region;
voice = this.options.voice || voice;
}
this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
this.ep = ep; this.ep = ep;
try { try {
if (!credentials) { if (!credentials) {
@@ -98,10 +146,7 @@ class TaskSay extends Task {
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
this.notifyError({ this.notifyError(`No speech credentials have been provisioned for ${vendor}`);
msg: 'TTS error',
details:`No speech credentials provisioned for selected vendor ${vendor}`
});
throw new Error('no provisioned speech credentials for TTS'); throw new Error('no provisioned speech credentials for TTS');
} }
// synthesize all of the text elements // synthesize all of the text elements
@@ -119,17 +164,18 @@ class TaskSay extends Task {
'tts.voice': voice 'tts.voice': voice
}); });
try { try {
const {filePath, servedFromCache, rtt} = await synthAudio(stats, { if (vendor === 'microsoft' && this.synthesizer.azureServiceEndpoint) {
account_sid: cs.accountSid, credentials.use_custom_tts = true;
credentials.custom_tts_endpoint = this.synthesizer.azureServiceEndpoint;
}
const {filePath, servedFromCache} = await synthAudio(stats, {
text, text,
vendor, vendor,
language, language,
voice, voice,
engine, engine,
model,
salt, salt,
credentials, credentials
disableTtsCache : this.disableTtsCache
}); });
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`); this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
if (filePath) cs.trackTmpFile(filePath); if (filePath) cs.trackTmpFile(filePath);
@@ -140,33 +186,24 @@ class TaskSay extends Task {
} }
span.setAttributes({'tts.cached': servedFromCache}); span.setAttributes({'tts.cached': servedFromCache});
span.end(); span.end();
if (!servedFromCache && rtt) {
this.notifyStatus({
event: 'synthesized-audio',
vendor,
language,
characters: text.length,
elapsedTime: rtt
});
}
return filePath; return filePath;
} catch (err) { } catch (err) {
this.logger.info({err}, 'Error synthesizing tts'); this.logger.info({err}, 'Error synthesizing tts');
span.end(); span.end();
writeAlerts({ writeAlerts({
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor, vendor,
detail: err.message detail: err.message
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.notifyError({msg: 'TTS error', details: err.message || err}); this.notifyError(err.message || err);
return; return;
} }
}; };
const arr = this.text.map((t) => generateAudio(t)); const arr = this.text.map((t) => generateAudio(t));
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length); const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
this.notifyStatus({event: 'start-playback'}); this.logger.debug({filepath}, 'synthesized files for tts');
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) { while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
let segment = 0; let segment = 0;
@@ -198,7 +235,6 @@ class TaskSay extends Task {
this.killPlayToConfMember(this.ep, memberId, confName); this.killPlayToConfMember(this.ep, memberId, confName);
} }
else { else {
this.notifyStatus({event: 'kill-playback'});
this.ep.api('uuid_break', this.ep.uuid); this.ep.api('uuid_break', this.ep.uuid);
} }
} }

View File

@@ -47,17 +47,7 @@ class TaskSipRefer extends Task {
/* if we fail, fall through to next verb. If success, we should get BYE from far end */ /* if we fail, fall through to next verb. If success, we should get BYE from far end */
if (this.referStatus === 202) { if (this.referStatus === 202) {
this._notifyTimer = setTimeout(() => {
this.logger.info('TaskSipRefer:exec - no NOTIFY received in 15 secs, exiting');
this.performAction({refer_status: this.referStatus})
.catch((err) => this.logger.error(err, 'TaskSipRefer:exec - error performing action'));
this.notifyTaskDone();
}, 15000);
await this.awaitTaskDone(); await this.awaitTaskDone();
if (this._notifyTimer) {
clearTimeout(this._notifyTimer);
this._notifyTimer = null;
}
} }
else { else {
await this.performAction({refer_status: this.referStatus}); await this.performAction({refer_status: this.referStatus});
@@ -81,10 +71,10 @@ class TaskSipRefer extends Task {
const contentType = req.get('Content-Type'); const contentType = req.get('Content-Type');
this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`); this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`);
if (contentType?.includes('message/sipfrag')) { if (contentType === 'message/sipfrag') {
const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body); const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body);
if (arr) { if (arr) {
const status = typeof arr[1] === 'string' ? parseInt(arr[1], 10) : arr[1]; const status = arr[1];
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`); this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
if (this.eventHook) { if (this.eventHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();

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

@@ -0,0 +1,558 @@
{
"sip:decline": {
"properties": {
"status": "number",
"reason": "string",
"headers": "object"
},
"required": [
"status"
]
},
"sip:request": {
"properties": {
"method": "string",
"body": "string",
"headers": "object",
"actionHook": "object|string"
},
"required": [
"method"
]
},
"sip:refer": {
"properties": {
"referTo": "string",
"referredBy": "string",
"headers": "object",
"actionHook": "object|string",
"eventHook": "object|string"
},
"required": [
"referTo"
]
},
"config": {
"properties": {
"synthesizer": "#synthesizer",
"recognizer": "#recognizer",
"bargeIn": "#bargeIn",
"record": "#recordOptions",
"amd": "#amd"
},
"required": []
},
"bargeIn": {
"properties": {
"enable": "boolean",
"sticky": "boolean",
"actionHook": "object|string",
"input": "array",
"finishOnKey": "string",
"numDigits": "number",
"minDigits": "number",
"maxDigits": "number",
"interDigitTimeout": "number",
"dtmfBargein": "boolean",
"minBargeinWordCount": "number"
},
"required": [
"enable"
]
},
"dequeue": {
"properties": {
"name": "string",
"actionHook": "object|string",
"timeout": "number",
"beep": "boolean"
},
"required": [
"name"
]
},
"enqueue": {
"properties": {
"name": "string",
"actionHook": "object|string",
"waitHook": "object|string",
"_": "object"
},
"required": [
"name"
]
},
"leave": {
"properties": {
}
},
"hangup": {
"properties": {
"headers": "object"
},
"required": [
]
},
"play": {
"properties": {
"url": "string|array",
"loop": "number|string",
"earlyMedia": "boolean",
"seekOffset": "number|string",
"timeoutSecs": "number|string",
"actionHook": "object|string"
},
"required": [
"url"
]
},
"say": {
"properties": {
"text": "string|array",
"loop": "number|string",
"synthesizer": "#synthesizer",
"earlyMedia": "boolean"
},
"required": [
"text"
]
},
"gather": {
"properties": {
"actionHook": "object|string",
"finishOnKey": "string",
"input": "array",
"numDigits": "number",
"minDigits": "number",
"maxDigits": "number",
"interDigitTimeout": "number",
"partialResultHook": "object|string",
"speechTimeout": "number",
"listenDuringPrompt": "boolean",
"dtmfBargein": "boolean",
"bargein": "boolean",
"minBargeinWordCount": "number",
"timeout": "number",
"recognizer": "#recognizer",
"play": "#play",
"say": "#say"
},
"required": [
]
},
"conference": {
"properties": {
"name": "string",
"beep": "boolean",
"startConferenceOnEnter": "boolean",
"endConferenceOnExit": "boolean",
"maxParticipants": "number",
"joinMuted": "boolean",
"actionHook": "object|string",
"waitHook": "object|string",
"statusEvents": "array",
"statusHook": "object|string",
"enterHook": "object|string",
"record": "#record"
},
"required": [
"name"
]
},
"dial": {
"properties": {
"actionHook": "object|string",
"answerOnBridge": "boolean",
"callerId": "string",
"confirmHook": "object|string",
"referHook": "object|string",
"dialMusic": "string",
"dtmfCapture": "object",
"dtmfHook": "object|string",
"headers": "object",
"listen": "#listen",
"target": ["#target"],
"timeLimit": "number",
"timeout": "number",
"proxy": "string",
"transcribe": "#transcribe",
"amd": "#amd"
},
"required": [
"target"
]
},
"dialogflow": {
"properties": {
"credentials": "object|string",
"project": "string",
"environment": "string",
"region": {
"type": "string",
"enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"]
},
"lang": "string",
"actionHook": "object|string",
"eventHook": "object|string",
"events": "[string]",
"welcomeEvent": "string",
"welcomeEventParams": "object",
"noInputTimeout": "number",
"noInputEvent": "string",
"passDtmfAsTextInput": "boolean",
"thinkingMusic": "string",
"tts": "#synthesizer",
"bargein": "boolean"
},
"required": [
"project",
"credentials",
"lang"
]
},
"dtmf": {
"properties": {
"dtmf": "string",
"duration": "number"
},
"required": [
"dtmf"
]
},
"lex": {
"properties": {
"botId": "string",
"botAlias": "string",
"credentials": "object",
"region": "string",
"locale": "string",
"intent": "#lexIntent",
"welcomeMessage": "string",
"metadata": "object",
"bargein": "boolean",
"passDtmf": "boolean",
"actionHook": "object|string",
"eventHook": "object|string",
"noInputTimeout": "number",
"tts": "#synthesizer"
},
"required": [
"botId",
"botAlias",
"region",
"credentials"
]
},
"listen": {
"properties": {
"actionHook": "object|string",
"auth": "#auth",
"finishOnKey": "string",
"maxLength": "number",
"metadata": "object",
"mixType": {
"type": "string",
"enum": ["mono", "stereo", "mixed"]
},
"passDtmf": "boolean",
"playBeep": "boolean",
"sampleRate": "number",
"timeout": "number",
"transcribe": "#transcribe",
"url": "string",
"wsAuth": "#auth",
"earlyMedia": "boolean"
},
"required": [
"url"
]
},
"message": {
"properties": {
"carrier": "string",
"account_sid": "string",
"message_sid": "string",
"to": "string",
"from": "string",
"text": "string",
"media": "string|array",
"actionHook": "object|string"
},
"required": [
"to",
"from"
]
},
"pause": {
"properties": {
"length": "number"
},
"required": [
"length"
]
},
"rasa": {
"properties": {
"url": "string",
"recognizer": "#recognizer",
"tts": "#synthesizer",
"prompt": "string",
"actionHook": "object|string",
"eventHook": "object|string"
},
"required": [
"url"
]
},
"record": {
"properties": {
"path": "string"
},
"required": [
"path"
]
},
"recordOptions": {
"properties": {
"action": {
"type": "string",
"enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"]
},
"recordingID": "string",
"siprecServerURL": "string"
},
"required": [
"action"
]
},
"redirect": {
"properties": {
"actionHook": "object|string"
},
"required": [
"actionHook"
]
},
"rest:dial": {
"properties": {
"account_sid": "string",
"application_sid": "string",
"call_hook": "object|string",
"call_status_hook": "object|string",
"from": "string",
"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": {
"data": "object"
},
"required": [
"data"
]
},
"transcribe": {
"properties": {
"transcriptionHook": "string",
"recognizer": "#recognizer",
"earlyMedia": "boolean"
},
"required": [
"recognizer"
]
},
"target": {
"properties": {
"type": {
"type": "string",
"enum": ["phone", "sip", "user", "teams"]
},
"confirmHook": "object|string",
"method": {
"type": "string",
"enum": ["GET", "POST"]
},
"headers": "object",
"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", "default"]
},
"language": "string",
"voice": "string",
"engine": {
"type": "string",
"enum": ["standard", "neural"]
},
"gender": {
"type": "string",
"enum": ["MALE", "FEMALE", "NEUTRAL"]
},
"azureServiceEndpoint": "string"
},
"required": [
"vendor"
]
},
"recognizer": {
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "microsoft", "default"]
},
"language": "string",
"vad": "#vad",
"hints": "array",
"hintsBoost": "number",
"altLanguages": "array",
"profanityFilter": "boolean",
"interim": "boolean",
"singleUtterance": "boolean",
"dualChannel": "boolean",
"separateRecognitionPerChannel": "boolean",
"punctuation": "boolean",
"enhancedModel": "boolean",
"words": "boolean",
"diarization": "boolean",
"diarizationMinSpeakers": "number",
"diarizationMaxSpeakers": "number",
"interactionType": {
"type": "string",
"enum": [
"unspecified",
"discussion",
"presentation",
"phone_call",
"voicemail",
"voice_search",
"voice_command",
"dictation"
]
},
"naicsCode": "number",
"identifyChannels": "boolean",
"vocabularyName": "string",
"vocabularyFilterName": "string",
"filterMethod": {
"type": "string",
"enum": [
"remove",
"mask",
"tag"
]
},
"model": "string",
"outputFormat": {
"type": "string",
"enum": [
"simple",
"detailed"
]
},
"profanityOption": {
"type": "string",
"enum": [
"masked",
"removed",
"raw"
]
},
"requestSnr": "boolean",
"initialSpeechTimeoutMs": "number",
"azureServiceEndpoint": "string",
"azureSttEndpointId": "string",
"asrDtmfTerminationDigit": "string",
"asrTimeout": "number",
"audioLogging": "boolean"
},
"required": [
"vendor"
]
},
"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"
}
}
}

View File

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

View File

@@ -3,18 +3,9 @@ const {
TaskName, TaskName,
TaskPreconditions, TaskPreconditions,
GoogleTranscriptionEvents, GoogleTranscriptionEvents,
NuanceTranscriptionEvents,
AwsTranscriptionEvents,
AzureTranscriptionEvents, AzureTranscriptionEvents,
DeepgramTranscriptionEvents, AwsTranscriptionEvents
SonioxTranscriptionEvents,
IbmTranscriptionEvents,
NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('../utils/constants'); } = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications');
const STT_LISTEN_SPAN_NAME = 'stt-listen';
class TaskTranscribe extends Task { class TaskTranscribe extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
@@ -22,40 +13,49 @@ class TaskTranscribe extends Task {
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.parentTask = parentTask; this.parentTask = parentTask;
const {
setChannelVarsForStt,
normalizeTranscription,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
} = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription;
this.removeSpeechListeners = removeSpeechListeners;
this.compileSonioxTranscripts = compileSonioxTranscripts;
this.transcriptionHook = this.data.transcriptionHook; this.transcriptionHook = this.data.transcriptionHook;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia); this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
if (this.data.recognizer) { const recognizer = this.data.recognizer;
const recognizer = this.data.recognizer; this.vendor = recognizer.vendor;
this.vendor = recognizer.vendor; this.language = recognizer.language;
this.language = recognizer.language; this.interim = !!recognizer.interim;
/* let credentials be supplied in the recognizer object at runtime */ this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
this.interim = !!recognizer.interim; /* vad: if provided, we dont connect to recognizer until voice activity is detected */
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel; const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
this.vad = {enable, voiceMs, mode};
this.data.recognizer.hints = this.data.recognizer.hints || []; /* google-specific options */
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || []; this.hints = recognizer.hints || [];
} this.hintsBoost = recognizer.hintsBoost;
else this.data.recognizer = {hints: [], altLanguages: []}; this.profanityFilter = recognizer.profanityFilter;
this.punctuation = !!recognizer.punctuation;
this.enhancedModel = !!recognizer.enhancedModel;
this.model = recognizer.model || 'phone_call';
this.words = !!recognizer.words;
this.singleUtterance = recognizer.singleUtterance || false;
this.diarization = !!recognizer.diarization;
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
this.interactionType = recognizer.interactionType || 'unspecified';
this.naicsCode = recognizer.naicsCode || 0;
this.altLanguages = recognizer.altLanguages || [];
/* buffer for soniox transcripts */ /* aws-specific options */
this._sonioxTranscripts = []; this.identifyChannels = !!recognizer.identifyChannels;
this.vocabularyName = recognizer.vocabularyName;
this.vocabularyFilterName = recognizer.vocabularyFilterName;
this.filterMethod = recognizer.filterMethod;
this.childSpan = [null, null]; /* microsoft options */
this.outputFormat = recognizer.outputFormat || 'simple';
this.profanityOption = recognizer.profanityOption || 'raw';
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
this.azureSttEndpointId = recognizer.azureSttEndpointId;
this.azureAudioLogging = recognizer.audioLogging;
} }
get name() { return TaskName.Transcribe; } get name() { return TaskName.Transcribe; }
@@ -63,38 +63,28 @@ class TaskTranscribe extends Task {
async exec(cs, {ep, ep2}) { async exec(cs, {ep, ep2}) {
super.exec(cs); super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
if (cs.hasGlobalSttHints) { if (cs.hasGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints; const {hints, hintsBoost} = cs.globalSttHints;
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints); this.hints = this.hints.concat(hints);
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost; if (!this.hintsBoost && hintsBoost) this.hintsBoost = hintsBoost;
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost}, this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
'Transcribe:exec - applying global sttHints'); 'Transcribe:exec - applying global `sttHints');
} }
if (cs.hasAltLanguages) { if (cs.hasAltLanguages) {
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages); this.altLanguages = this.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages}, this.logger.debug({altLanguages: this.altLanguages},
'Transcribe:exec - applying altLanguages'); 'Gather:exec - applying altLanguages');
} }
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) { if (cs.hasGlobalSttPunctuation) {
this.data.recognizer.punctuation = cs.globalSttPunctuation; this.punctuation = cs.globalSttPunctuation;
} }
this.ep = ep; this.ep = ep;
this.ep2 = ep2; this.ep2 = ep2;
if ('default' === this.vendor || !this.vendor) { if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
this.vendor = cs.speechRecognizerVendor; if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor; this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (!this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
try { try {
if (!this.sttCredentials) { if (!this.sttCredentials) {
@@ -107,22 +97,6 @@ class TaskTranscribe extends Task {
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
throw new Error('no provisioned speech credentials for TTS'); throw new Error('no provisioned speech credentials for TTS');
} }
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
/* get nuance access token */
const {client_id, secret} = this.sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
this.logger.debug({client_id},
`Transcribe:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token};
}
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
/* get ibm access token */
const {stt_api_key, stt_region} = this.sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
}
await this._startTranscribing(cs, ep, 1); await this._startTranscribing(cs, ep, 1);
if (this.separateRecognitionPerChannel && ep2) { if (this.separateRecognitionPerChannel && ep2) {
await this._startTranscribing(cs, ep2, 2); await this._startTranscribing(cs, ep2, 2);
@@ -136,7 +110,14 @@ class TaskTranscribe extends Task {
this.logger.info(err, 'TaskTranscribe:exec - error'); this.logger.info(err, 'TaskTranscribe:exec - error');
this.parentTask && this.parentTask.emit('error', err); this.parentTask && this.parentTask.emit('error', err);
} }
this.removeSpeechListeners(ep); ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
} }
async kill(cs) { async kill(cs) {
@@ -160,93 +141,126 @@ class TaskTranscribe extends Task {
} }
async _startTranscribing(cs, ep, channel) { async _startTranscribing(cs, ep, channel) {
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer); const opts = {};
switch (this.vendor) {
case 'google':
this.bugname = 'google_transcribe';
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected,
this._onNoAudio.bind(this, cs, ep, channel));
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
break;
case 'aws': if (this.vad.enable) {
case 'polly': opts.START_RECOGNIZING_ON_VAD = 1;
this.bugname = 'aws_transcribe'; if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected,
this._onNoAudio.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
break;
case 'microsoft':
this.bugname = 'azure_transcribe';
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected,
this._onNoAudio.bind(this, cs, ep, channel));
break;
case 'nuance':
this.bugname = 'nuance_transcribe';
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep, channel));
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep, channel));
break;
case 'deepgram':
this.bugname = 'deepgram_transcribe';
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect,
this._onDeepgramConnect.bind(this, cs, ep, channel));
ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
this._onDeepGramConnectFailure.bind(this, cs, ep, channel));
break;
case 'soniox':
this.bugname = 'soniox_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
break;
case 'ibm':
this.bugname = 'ibm_transcribe';
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(IbmTranscriptionEvents.Connect,
this._onIbmConnect.bind(this, cs, ep, channel));
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onIbmConnectFailure.bind(this, cs, ep, channel));
break;
case 'nvidia':
this.bugname = 'nvidia_transcribe';
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
break;
default:
throw new Error(`Invalid vendor ${this.vendor}`);
} }
/* common handler for all stt engine errors */ ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep)); this._onTranscription.bind(this, cs, ep, channel));
await ep.set(opts) ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
.catch((err) => this.logger.info(err, 'Error setting channel variables')); ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
if (this.vendor === 'google') {
this.bugname = 'google_transcribe';
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
//['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
['singleUtterance', 'GOOGLE_SPEECH_SINGLE_UTTERANCE'],
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
else if (this[arr[0]] === false) opts[arr[1]] = false;
});
if (this.hints.length > 0) {
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
if (typeof this.hintsBoost === 'number') {
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
}
}
if (this.altLanguages.length > 0) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
else opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
if ('unspecified' !== this.interactionType) {
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
}
opts.GOOGLE_SPEECH_MODEL = this.model;
if (this.diarization && this.diarizationMinSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
}
if (this.diarization && this.diarizationMaxSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
}
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
}
else if (this.vendor === 'aws') {
this.bugname = 'aws_transcribe';
[
['diarization', 'AWS_SHOW_SPEAKER_LABEL'],
['identifyChannels', 'AWS_ENABLE_CHANNEL_IDENTIFICATION']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
});
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
if (this.vocabularyFilterName) {
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
}
if (this.sttCredentials) {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
}
else {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_REGION: process.env.AWS_REGION
});
}
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
}
else if (this.vendor === 'microsoft') {
this.bugname = 'azure_transcribe';
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': api_key,
'AZURE_REGION': region
});
if (this.azureSttEndpointId) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
}
else if (use_custom_stt && custom_stt_endpoint) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
}
if (this.hints && this.hints.length > 0) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
if (this.altLanguages.length > 0) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
else opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
if (this.azureAudioLogging) opts.AZURE_AUDIO_LOGGING = 1;
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
if (this.outputFormat !== 'simple') opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with azure'));
}
await this._transcribe(ep); await this._transcribe(ep);
/* start child span for this channel */
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
this.childSpan[channel - 1] = {span, ctx};
} }
async _transcribe(ep) { async _transcribe(ep) {
@@ -259,74 +273,50 @@ class TaskTranscribe extends Task {
}); });
} }
async _onTranscription(cs, ep, channel, evt, fsEvent) { _onTranscription(cs, ep, channel, evt, fsEvent) {
// make sure this is not a transcript from answering machine detection // make sure this is not a transcript from answering machine detection
const bugname = fsEvent.getHeader('media-bugname'); const bugname = fsEvent.getHeader('media-bugname');
if (bugname && this.bugname !== bugname) return; if (bugname && this.bugname !== bugname) return;
if (this.vendor === 'ibm') { this.logger.debug({evt, channel}, 'TaskTranscribe:_onTranscription');
if (evt?.state === 'listening') return; if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
} if ('microsoft' === this.vendor) {
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization'); const nbest = evt.NBest;
const language_code = evt.PrimaryLanguage?.Language || this.language;
const alternatives = nbest ? nbest.map((n) => {
return {
confidence: n.Confidence,
transcript: n.Display
};
}) :
[
{
transcript: evt.DisplayText || evt.Text
}
];
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language); const newEvent = {
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription'); is_final: evt.RecognitionStatus === 'Success',
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)) {
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening');
}
else {
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, listen again');
this._transcribe(ep);
}
return;
}
if (this.vendor === 'soniox') {
/* compile transcripts into one */
this._sonioxTranscripts.push(evt.vendor.finalWords);
if (evt.is_final) {
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
this._sonioxTranscripts = [];
}
}
/* we've got a transcript, so end the otel child span for this channel */
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({
channel, channel,
'stt.resolve': 'transcript', language_code,
'stt.result': JSON.stringify(evt) alternatives
}); };
this.childSpan[channel - 1].span.end(); evt = newEvent;
} }
if (evt.alternatives[0].transcript === '' && !cs.callGone && !this.killed) {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
return this._transcribe(ep);
}
evt.channel_tag = channel;
if (this.transcriptionHook) { if (this.transcriptionHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
try { this.cs.requestor.request('verb:hook', this.transcriptionHook,
const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, { Object.assign({speech: evt}, this.cs.callInfo), httpHeaders)
...this.cs.callInfo, .catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
...httpHeaders,
speech: evt
});
this.logger.info({json}, 'sent transcriptionHook');
if (json && Array.isArray(json) && !this.parentTask) {
const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.cs.replaceApplication(tasks);
}
}
} catch (err) {
this.logger.info(err, 'TranscribeTask:_onTranscription error');
}
} }
if (this.parentTask) { if (this.parentTask) {
this.parentTask.emit('transcription', evt); this.parentTask.emit('transcription', evt);
@@ -336,44 +326,16 @@ class TaskTranscribe extends Task {
this._clearTimer(); this._clearTimer();
this.notifyTaskDone(); this.notifyTaskDone();
} }
else {
/* start another child span for this channel */
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
this.childSpan[channel - 1] = {span, ctx};
}
} }
_onNoAudio(cs, ep, channel) { _onNoAudio(cs, ep, channel) {
this.logger.debug(`TaskTranscribe:_onNoAudio restarting transcription on channel ${channel}`); this.logger.debug(`TaskTranscribe:_onNoAudio restarting transcription on channel ${channel}`);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({
channel,
'stt.resolve': 'timeout'
});
this.childSpan[channel - 1].span.end();
}
this._transcribe(ep); this._transcribe(ep);
/* start new child span for this channel */
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
this.childSpan[channel - 1] = {span, ctx};
} }
_onMaxDurationExceeded(cs, ep, channel) { _onMaxDurationExceeded(cs, ep, channel) {
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded restarting transcription on channel ${channel}`); this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded restarting transcription on channel ${channel}`);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({
channel,
'stt.resolve': 'max duration exceeded'
});
this.childSpan[channel - 1].span.end();
}
this._transcribe(ep); this._transcribe(ep);
/* start new child span for this channel */
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
this.childSpan[channel - 1] = {span, ctx};
} }
_clearTimer() { _clearTimer() {
@@ -382,80 +344,6 @@ class TaskTranscribe extends Task {
this._timer = null; this._timer = null;
} }
} }
_onDeepgramConnect(_cs, _ep) {
this.logger.debug('TaskTranscribe:_onDeepgramConnect');
}
_onDeepGramConnectFailure(cs, _ep, channel, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskTranscribe:_onDeepgramConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
vendor: 'deepgram',
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
this.notifyError(`Failed connecting to speech vendor deepgram: ${reason}`);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({
channel,
'stt.resolve': 'connection failure'
});
this.childSpan[channel - 1].span.end();
}
this.notifyTaskDone();
}
_onIbmConnect(_cs, _ep) {
this.logger.debug('TaskTranscribe:_onIbmConnect');
}
_onIbmConnectFailure(cs, _ep, channel, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskTranscribe:_onIbmConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
vendor: 'ibm',
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
this.notifyError(`Failed connecting to speech vendor IBM: ${reason}`);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({
channel,
'stt.resolve': 'connection failure'
});
this.childSpan[channel - 1].span.end();
}
this.notifyTaskDone();
}
_onIbmError(cs, _ep, _channel, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onIbmError');
}
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
} }
module.exports = TaskTranscribe; module.exports = TaskTranscribe;

View File

@@ -1,21 +1,14 @@
const Emitter = require('events'); const Emitter = require('events');
const {readFile} = require('fs'); const {readFile} = require('fs');
const { const {
TaskName,
GoogleTranscriptionEvents, GoogleTranscriptionEvents,
AwsTranscriptionEvents, AwsTranscriptionEvents,
AzureTranscriptionEvents, AzureTranscriptionEvents,
NuanceTranscriptionEvents,
NvidiaTranscriptionEvents,
IbmTranscriptionEvents,
SonioxTranscriptionEvents,
DeepgramTranscriptionEvents,
JambonzTranscriptionEvents,
AmdEvents, AmdEvents,
AvmdEvents AvmdEvents
} = require('./constants'); } = require('./constants');
const bugname = 'amd_bug'; const bugname = 'amd_bug';
const {VMD_HINTS_FILE} = require('../config'); const {VMD_HINTS_FILE} = process.env;
let voicemailHints = []; let voicemailHints = [];
const updateHints = async(file, callback) => { const updateHints = async(file, callback) => {
@@ -61,11 +54,6 @@ class Amd extends Emitter {
this.thresholdWordCount = opts.thresholdWordCount || 9; this.thresholdWordCount = opts.thresholdWordCount || 9;
const {normalizeTranscription} = require('./transcription-utils')(logger); const {normalizeTranscription} = require('./transcription-utils')(logger);
this.normalizeTranscription = normalizeTranscription; this.normalizeTranscription = normalizeTranscription;
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
this.getNuanceAccessToken = getNuanceAccessToken;
this.getIbmAccessToken = getIbmAccessToken;
const {setChannelVarsForStt} = require('./transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
const { const {
noSpeechTimeoutMs = 5000, noSpeechTimeoutMs = 5000,
@@ -196,7 +184,7 @@ module.exports = (logger) => {
const {vendor, language} = ep.amd; const {vendor, language} = ep.amd;
ep.startTranscription({ ep.startTranscription({
vendor, vendor,
locale: language, language,
interim: true, interim: true,
bugname bugname
}).catch((err) => { }).catch((err) => {
@@ -241,92 +229,52 @@ module.exports = (logger) => {
const startAmd = async(cs, ep, task, opts) => { const startAmd = async(cs, ep, task, opts) => {
const amd = ep.amd = new Amd(logger, cs, opts); const amd = ep.amd = new Amd(logger, cs, opts);
const {vendor, language} = amd; const {vendor, language, sttCredentials} = amd;
let sttCredentials = amd.sttCredentials; const sttOpts = {};
const hints = voicemailHints[language] || []; const hints = voicemailHints[language] || [];
if (vendor === 'nuance' && sttCredentials.client_id) {
/* get nuance access token */
const {getNuanceAccessToken} = amd;
const {client_id, secret} = sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
sttCredentials = {...sttCredentials, access_token};
}
else if (vendor == 'ibm' && sttCredentials.stt_api_key) {
/* get ibm access token */
const {getIbmAccessToken} = amd;
const {stt_api_key, stt_region} = sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
sttCredentials = {...sttCredentials, access_token, stt_region};
}
/* set stt options */ /* set stt options */
logger.info(`starting amd for vendor ${vendor} and language ${language}`); logger.info(`starting amd for vendor ${vendor} and language ${language}`);
const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, { if ('google' === vendor) {
vendor, sttOpts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(sttCredentials.credentials);
hints, sttOpts.GOOGLE_SPEECH_USE_ENHANCED = true;
enhancedModel: true, sttOpts.GOOGLE_SPEECH_HINTS = hints.join(',');
altLanguages: opts.recognizer?.altLanguages || [], if (opts.recognizer?.altLanguages) {
initialSpeechTimeoutMs: opts.resolveTimeoutMs, sttOpts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
}); }
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, onEndOfUtterance.bind(null, cs, ep, task));
}
else if (['aws', 'polly'].includes(vendor)) {
Object.assign(sttOpts, {
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
AWS_REGION: sttCredentials.region
});
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
}
else if ('microsoft' === vendor) {
Object.assign(sttOpts, {
'AZURE_SUBSCRIPTION_KEY': sttCredentials.api_key,
'AZURE_REGION': sttCredentials.region
});
sttOpts.AZURE_SPEECH_HINTS = hints.join(',');
if (opts.recognizer?.altLanguages) {
sttOpts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
}
sttOpts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = opts.resolveTimeoutMs || 20000;
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
}
logger.debug({sttOpts}, 'startAmd: setting channel vars');
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables')); await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
amd.transcriptionHandler = onTranscription.bind(null, cs, ep, task);
amd.EndOfUtteranceHandler = onEndOfUtterance.bind(null, cs, ep, task);
amd.noSpeechHandler = onNoSpeechDetected.bind(null, cs, ep, task);
switch (vendor) {
case 'google':
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, amd.transcriptionHandler);
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, amd.EndOfUtteranceHandler);
break;
case 'aws':
case 'polly':
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'microsoft':
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, amd.transcriptionHandler);
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, amd.noSpeechHandler);
break;
case 'nuance':
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'deepgram':
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'soniox':
amd.bugname = 'soniox_amd_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'ibm':
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'nvidia':
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
default:
if (vendor.startsWith('custom:')) {
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
}
else {
throw new Error(`Invalid vendor ${this.vendor}`);
}
}
amd amd
.on(AmdEvents.NoSpeechDetected, (evt) => { .on(AmdEvents.NoSpeechDetected, (evt) => {
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt}); task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
try { try {
stopAmd(ep, task); ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) { } catch (err) {
logger.info({err}, 'Error stopping transcription'); logger.info({err}, 'Error stopping transcription');
} }
@@ -334,7 +282,7 @@ module.exports = (logger) => {
.on(AmdEvents.HumanDetected, (evt) => { .on(AmdEvents.HumanDetected, (evt) => {
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt}); task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
try { try {
stopAmd(ep, task); ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) { } catch (err) {
logger.info({err}, 'Error stopping transcription'); logger.info({err}, 'Error stopping transcription');
} }
@@ -345,7 +293,7 @@ module.exports = (logger) => {
.on(AmdEvents.DecisionTimeout, (evt) => { .on(AmdEvents.DecisionTimeout, (evt) => {
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt}); task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
try { try {
stopAmd(ep, task); ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) { } catch (err) {
logger.info({err}, 'Error stopping transcription'); logger.info({err}, 'Error stopping transcription');
} }
@@ -353,7 +301,7 @@ module.exports = (logger) => {
.on(AmdEvents.ToneTimeout, (evt) => { .on(AmdEvents.ToneTimeout, (evt) => {
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt}); //task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
try { try {
stopAmd(ep, task); ep.connected && ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
} catch (err) { } catch (err) {
logger.info({err}, 'Error stopping avmd'); logger.info({err}, 'Error stopping avmd');
} }
@@ -361,7 +309,7 @@ module.exports = (logger) => {
.on(AmdEvents.MachineStoppedSpeaking, () => { .on(AmdEvents.MachineStoppedSpeaking, () => {
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking}); task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
try { try {
stopAmd(ep, task); ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) { } catch (err) {
logger.info({err}, 'Error stopping transcription'); logger.info({err}, 'Error stopping transcription');
} }
@@ -380,19 +328,6 @@ module.exports = (logger) => {
if (ep.amd) { if (ep.amd) {
vendor = ep.amd.vendor; vendor = ep.amd.vendor;
ep.amd.stopAllTimers(); ep.amd.stopAllTimers();
ep.removeListener(GoogleTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(GoogleTranscriptionEvents.EndOfUtterance, ep.amd.EndOfUtteranceHandler);
ep.removeListener(AwsTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(AzureTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(AzureTranscriptionEvents.NoSpeechDetected, ep.amd.noSpeechHandler);
ep.removeListener(NuanceTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(DeepgramTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(SonioxTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(IbmTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(NvidiaTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(JambonzTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.amd = null; ep.amd = null;
} }

View File

@@ -1,12 +1,7 @@
const Emitter = require('events'); const Emitter = require('events');
const bent = require('bent'); const bent = require('bent');
const assert = require('assert'); const assert = require('assert');
const { const PORT = process.env.AWS_SNS_PORT || 3010;
AWS_REGION,
AWS_SNS_PORT: PORT,
AWS_SNS_TOPIC_ARM,
AWS_SNS_PORT_MAX,
} = require('../config');
const {LifeCycleEvents} = require('./constants'); const {LifeCycleEvents} = require('./constants');
const express = require('express'); const express = require('express');
const app = express(); const app = express();
@@ -18,7 +13,7 @@ const {Parser} = require('xml2js');
const parser = new Parser(); const parser = new Parser();
const {validatePayload} = require('verify-aws-sns-signature'); const {validatePayload} = require('verify-aws-sns-signature');
AWS.config.update({region: AWS_REGION}); AWS.config.update({region: process.env.AWS_REGION});
class SnsNotifier extends Emitter { class SnsNotifier extends Emitter {
constructor(logger) { constructor(logger) {
@@ -36,8 +31,8 @@ class SnsNotifier extends Emitter {
_handleErrors(logger, app, resolve, reject, e) { _handleErrors(logger, app, resolve, reject, e) {
if (e.code === 'EADDRINUSE' && if (e.code === 'EADDRINUSE' &&
AWS_SNS_PORT_MAX && process.env.AWS_SNS_PORT_MAX &&
e.port < AWS_SNS_PORT_MAX) { e.port < process.env.AWS_SNS_PORT_MAX) {
logger.info(`SNS lifecycle server failed to bind port on ${e.port}, will try next port`); logger.info(`SNS lifecycle server failed to bind port on ${e.port}, will try next port`);
const server = this._doListen(logger, app, ++e.port, resolve); const server = this._doListen(logger, app, ++e.port, resolve);
@@ -137,12 +132,12 @@ class SnsNotifier extends Emitter {
try { try {
const response = await sns.subscribe({ const response = await sns.subscribe({
Protocol: 'http', Protocol: 'http',
TopicArn: AWS_SNS_TOPIC_ARM, TopicArn: process.env.AWS_SNS_TOPIC_ARM,
Endpoint: this.snsEndpoint Endpoint: this.snsEndpoint
}).promise(); }).promise();
this.logger.info({response}, `response to SNS subscribe to ${AWS_SNS_TOPIC_ARM}`); this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) { } catch (err) {
this.logger.error({err}, `Error subscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`); this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
} }
} }
@@ -152,9 +147,9 @@ class SnsNotifier extends Emitter {
const response = await sns.unsubscribe({ const response = await sns.unsubscribe({
SubscriptionArn: this.subscriptionArn SubscriptionArn: this.subscriptionArn
}).promise(); }).promise();
this.logger.info({response}, `response to SNS unsubscribe to ${AWS_SNS_TOPIC_ARM}`); this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) { } catch (err) {
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`); this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
} }
} }

View File

@@ -2,7 +2,6 @@ const assert = require('assert');
const Emitter = require('events'); const Emitter = require('events');
const crypto = require('crypto'); const crypto = require('crypto');
const timeSeries = require('@jambonz/time-series'); const timeSeries = require('@jambonz/time-series');
const {NODE_ENV, JAMBONES_TIME_SERIES_HOST} = require('../config');
let alerter ; let alerter ;
class BaseRequestor extends Emitter { class BaseRequestor extends Emitter {
@@ -23,9 +22,9 @@ class BaseRequestor extends Emitter {
if (!alerter) { if (!alerter) {
alerter = timeSeries(logger, { alerter = timeSeries(logger, {
host: JAMBONES_TIME_SERIES_HOST, host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50, commitSize: 50,
commitInterval: 'test' === NODE_ENV ? 7 : 20 commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
}); });
} }
} }

View File

@@ -67,35 +67,6 @@
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded", "MaxDurationExceeded": "google_transcribe::max_duration_exceeded",
"VadDetected": "google_transcribe::vad_detected" "VadDetected": "google_transcribe::vad_detected"
}, },
"NuanceTranscriptionEvents": {
"Transcription": "nuance_transcribe::transcription",
"StartOfSpeech": "nuance_transcribe::start_of_speech",
"TranscriptionComplete": "nuance_transcribe::end_of_transcription",
"Error": "nuance_transcribe::error",
"VadDetected": "nuance_transcribe::vad_detected"
},
"NvidiaTranscriptionEvents": {
"Transcription": "nvidia_transcribe::transcription",
"StartOfSpeech": "nvidia_transcribe::start_of_speech",
"TranscriptionComplete": "nvidia_transcribe::end_of_transcription",
"Error": "nvidia_transcribe::error",
"VadDetected": "nvidia_transcribe::vad_detected"
},
"DeepgramTranscriptionEvents": {
"Transcription": "deepgram_transcribe::transcription",
"ConnectFailure": "deepgram_transcribe::connect_failed",
"Connect": "deepgram_transcribe::connect"
},
"SonioxTranscriptionEvents": {
"Transcription": "soniox_transcribe::transcription",
"Error": "soniox_transcribe::error"
},
"IbmTranscriptionEvents": {
"Transcription": "ibm_transcribe::transcription",
"ConnectFailure": "ibm_transcribe::connect_failed",
"Connect": "ibm_transcribe::connect",
"Error": "ibm_transcribe::error"
},
"AwsTranscriptionEvents": { "AwsTranscriptionEvents": {
"Transcription": "aws_transcribe::transcription", "Transcription": "aws_transcribe::transcription",
"EndOfTranscript": "aws_transcribe::end_of_transcript", "EndOfTranscript": "aws_transcribe::end_of_transcript",
@@ -110,12 +81,6 @@
"NoSpeechDetected": "azure_transcribe::no_speech_detected", "NoSpeechDetected": "azure_transcribe::no_speech_detected",
"VadDetected": "azure_transcribe::vad_detected" "VadDetected": "azure_transcribe::vad_detected"
}, },
"JambonzTranscriptionEvents": {
"Transcription": "jambonz_transcribe::transcription",
"ConnectFailure": "jambonz_transcribe::connect_failed",
"Connect": "jambonz_transcribe::connect",
"Error": "jambonz_transcribe::error"
},
"ListenEvents": { "ListenEvents": {
"Connect": "mod_audio_fork::connect", "Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed", "ConnectFailure": "mod_audio_fork::connect_failed",
@@ -157,7 +122,6 @@
"queue:status", "queue:status",
"dial:confirm", "dial:confirm",
"verb:hook", "verb:hook",
"verb:status",
"jambonz:error" "jambonz:error"
], ],
"RecordState": { "RecordState": {

View File

@@ -1,24 +1,19 @@
const {execSync} = require('child_process'); const {execSync} = require('child_process');
const {
JAMBONES_FREESWITCH,
NODE_ENV,
JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS,
} = require('../config');
const now = Date.now(); const now = Date.now();
const fsInventory = JAMBONES_FREESWITCH const fsInventory = process.env.JAMBONES_FREESWITCH
.split(',') .split(',')
.map((fs) => { .map((fs) => {
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs); const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
const opts = {address: arr[1], port: arr[2], secret: arr[3]}; const opts = {address: arr[1], port: arr[2], secret: arr[3]};
if (arr.length > 4) opts.advertisedAddress = arr[4]; if (arr.length > 4) opts.advertisedAddress = arr[4];
if (NODE_ENV === 'test') opts.listenAddress = '0.0.0.0'; if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
return opts; return opts;
}); });
const clearChannels = () => { const clearChannels = () => {
const {logger} = require('../..'); const {logger} = require('../..');
const pwd = fsInventory[0].secret; const pwd = fsInventory[0].secret;
const maxDurationMins = JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS; const maxDurationMins = process.env.JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS || 180;
const calls = execSync(`/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "show calls"`, {encoding: 'utf8'}) const calls = execSync(`/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "show calls"`, {encoding: 'utf8'})
.split('\n') .split('\n')

View File

@@ -20,16 +20,6 @@ WHERE vc.account_sid IS NULL
AND vc.service_provider_sid = AND vc.service_provider_sid =
(SELECT service_provider_sid from accounts where account_sid = ?) (SELECT service_provider_sid from accounts where account_sid = ?)
AND vc.name = ?`; AND vc.name = ?`;
const sqlQueryAccountPhoneNumber = `SELECT voip_carrier_sid
FROM phone_numbers pn
WHERE pn.account_sid = ?
AND pn.number = ?`;
const sqlQuerySPPhoneNumber = `SELECT voip_carrier_sid
FROM phone_numbers pn
WHERE pn.account_sid IS NULL
AND pn.service_provider_sid =
(SELECT service_provider_sid from accounts where account_sid = ?)
AND pn.number = ?`;
const speechMapper = (cred) => { const speechMapper = (cred) => {
const {credential, ...obj} = cred; const {credential, ...obj} = cred;
@@ -56,79 +46,56 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
} }
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret;
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));
obj.tts_api_key = o.tts_api_key;
obj.tts_region = o.tts_region;
obj.stt_api_key = o.stt_api_key;
obj.stt_region = o.stt_region;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
else if ('nvidia' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
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) { } catch (err) {
console.log(err);
} }
return obj; return obj;
}; };
const bucketCredentialDecrypt = (account) => {
const { bucket_credential } = account.account;
if (!bucket_credential || bucket_credential.vendor) return;
account.account.bucket_credential = JSON.parse(decrypt(bucket_credential));
};
module.exports = (logger, srf) => { module.exports = (logger, srf) => {
const {pool} = srf.locals.dbHelpers; const {pool} = srf.locals.dbHelpers;
const pp = pool.promise(); const pp = pool.promise();
const lookupAccountDetails = async(account_sid) => { const lookupAccountDetails = async(account_sid) => {
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, [account_sid]); const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, account_sid);
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`); if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
const [r2] = await pp.query(sqlSpeechCredentials, [account_sid]); const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
const speech = r2.map(speechMapper); const speech = r2.map(speechMapper);
/* add service provider creds unless we have that vendor at the account level */ /* search at the service provider level if we don't find it at the account level */
const [r3] = await pp.query(sqlSpeechCredentialsForSP, [account_sid]); const haveGoogle = speech.find((s) => s.vendor === 'google');
r3.forEach((s) => { const haveAws = speech.find((s) => s.vendor === 'aws');
if (!speech.find((s2) => s2.vendor === s.vendor)) { const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
speech.push(speechMapper(s)); const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
if (!haveGoogle || !haveAws || !haveMicrosoft) {
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
if (r3.length) {
if (!haveGoogle) {
const google = r3.find((s) => s.vendor === 'google');
if (google) speech.push(speechMapper(google));
}
if (!haveAws) {
const aws = r3.find((s) => s.vendor === 'aws');
if (aws) speech.push(speechMapper(aws));
}
if (!haveMicrosoft) {
const ms = r3.find((s) => s.vendor === 'microsoft');
if (ms) speech.push(speechMapper(ms));
}
if (!haveWellsaid) {
const wellsaid = r3.find((s) => s.vendor === 'wellsaid');
if (wellsaid) speech.push(speechMapper(wellsaid));
}
} }
}); }
const account = r[0];
bucketCredentialDecrypt(account);
return { return {
...account, ...r[0],
speech speech
}; };
}; };
const updateSpeechCredentialLastUsed = async(speech_credential_sid) => { const updateSpeechCredentialLastUsed = async(speech_credential_sid) => {
if (!speech_credential_sid) return;
const pp = pool.promise(); const pp = pool.promise();
const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?'; const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?';
try { try {
@@ -150,22 +117,9 @@ module.exports = (logger, srf) => {
} }
}; };
const lookupCarrierByPhoneNumber = async(account_sid, phoneNumber) => {
const pp = pool.promise();
try {
const [r] = await pp.query(sqlQueryAccountPhoneNumber, [account_sid, phoneNumber]);
if (r.length) return r[0].voip_carrier_sid;
const [r2] = await pp.query(sqlQuerySPPhoneNumber, [account_sid, phoneNumber]);
if (r2.length) return r2[0].voip_carrier_sid;
} catch (err) {
logger.error({err}, `lookupPhoneNumber: Error ${account_sid}:${phoneNumber}`);
}
};
return { return {
lookupAccountDetails, lookupAccountDetails,
updateSpeechCredentialLastUsed, updateSpeechCredentialLastUsed,
lookupCarrier, lookupCarrier
lookupCarrierByPhoneNumber
}; };
}; };

View File

@@ -1,11 +1,10 @@
const crypto = require('crypto'); const crypto = require('crypto');
const {LEGACY_CRYPTO, ENCRYPTION_SECRET, JWT_SECRET} = require('../config'); const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
const algorithm = LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256') const secretKey = crypto.createHash('sha256')
.update(ENCRYPTION_SECRET || JWT_SECRET) .update(String(process.env.JWT_SECRET))
.digest('base64') .digest('base64')
.substring(0, 32); .substr(0, 32);
const encrypt = (text) => { const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv); const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
@@ -26,8 +25,8 @@ const decrypt = (data) => {
throw err; throw err;
} }
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex')); const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrypted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]); const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
return decrypted.toString(); return decrpyted.toString();
}; };
module.exports = { module.exports = {

View File

@@ -1,7 +1,7 @@
const express = require('express'); const express = require('express');
const httpRoutes = require('../http-routes'); const httpRoutes = require('../http-routes');
const {PORT, HTTP_PORT_MAX} = require('../config'); const PORT = process.env.HTTP_PORT || 3000;
const doListen = (logger, app, port, resolve) => { const doListen = (logger, app, port, resolve) => {
const server = app.listen(port, () => { const server = app.listen(port, () => {
@@ -13,8 +13,8 @@ const doListen = (logger, app, port, resolve) => {
}; };
const handleErrors = (logger, app, resolve, reject, e) => { const handleErrors = (logger, app, resolve, reject, e) => {
if (e.code === 'EADDRINUSE' && if (e.code === 'EADDRINUSE' &&
HTTP_PORT_MAX && process.env.HTTP_PORT_MAX &&
e.port < HTTP_PORT_MAX) { e.port < process.env.HTTP_PORT_MAX) {
logger.info(`HTTP server failed to bind port on ${e.port}, will try next port`); logger.info(`HTTP server failed to bind port on ${e.port}, will try next port`);
const server = doListen(logger, app, ++e.port, resolve); const server = doListen(logger, app, ++e.port, resolve);

View File

@@ -5,12 +5,7 @@ const BaseRequestor = require('./base-requestor');
const {HookMsgTypes} = require('./constants.json'); const {HookMsgTypes} = require('./constants.json');
const snakeCaseKeys = require('./snakecase-keys'); const snakeCaseKeys = require('./snakecase-keys');
const pools = new Map(); const pools = new Map();
const { const HTTP_TIMEOUT = 10000;
HTTP_POOL,
HTTP_POOLSIZE,
HTTP_PIPELINING,
HTTP_TIMEOUT,
} = require('../config');
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64'); const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
@@ -39,15 +34,15 @@ class HttpRequestor extends BaseRequestor {
this._resource = u.resource; this._resource = u.resource;
this._port = u.port; this._port = u.port;
this._search = u.search; this._search = u.search;
this._usePools = HTTP_POOL && parseInt(HTTP_POOL); this._usePools = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
if (this._usePools) { if (this._usePools) {
if (pools.has(this._baseUrl)) { if (pools.has(this._baseUrl)) {
this.client = pools.get(this._baseUrl); this.client = pools.get(this._baseUrl);
} }
else { else {
const connections = HTTP_POOLSIZE ? parseInt(HTTP_POOLSIZE) : 10; const connections = process.env.HTTP_POOLSIZE ? parseInt(process.env.HTTP_POOLSIZE) : 10;
const pipelining = HTTP_PIPELINING ? parseInt(HTTP_PIPELINING) : 1; const pipelining = process.env.HTTP_PIPELINING ? parseInt(process.env.HTTP_PIPELINING) : 1;
const pool = this.client = new Pool(this._baseUrl, { const pool = this.client = new Pool(this._baseUrl, {
connections, connections,
pipelining pipelining
@@ -96,19 +91,6 @@ class HttpRequestor extends BaseRequestor {
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`); assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
const startAt = process.hrtime(); const startAt = process.hrtime();
/* 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);
if (type === 'session:redirect') {
this.close();
this.emit('handover', requestor);
}
return requestor.request('session:new', hook, params, httpHeaders);
}
let newClient; let newClient;
try { try {
let client, path, query; let client, path, query;

View File

@@ -1,22 +1,6 @@
const Mrf = require('drachtio-fsmrf'); const Mrf = require('drachtio-fsmrf');
const ip = require('ip'); const ip = require('ip');
const { const PORT = process.env.HTTP_PORT || 3000;
JAMBONES_MYSQL_HOST,
JAMBONES_MYSQL_USER,
JAMBONES_MYSQL_PASSWORD,
JAMBONES_MYSQL_DATABASE,
JAMBONES_MYSQL_CONNECTION_LIMIT,
JAMBONES_MYSQL_PORT,
JAMBONES_FREESWITCH,
JAMBONES_REDIS_HOST,
JAMBONES_REDIS_PORT,
JAMBONES_REDIS_SENTINELS,
SMPP_URL,
JAMBONES_TIME_SERIES_HOST,
JAMBONES_ESL_LISTEN_ADDRESS,
PORT,
NODE_ENV,
} = require('../config');
const assert = require('assert'); const assert = require('assert');
function initMS(logger, wrapper, ms) { function initMS(logger, wrapper, ms) {
@@ -58,18 +42,18 @@ function installSrfLocals(srf, logger) {
let idxStart = 0; let idxStart = 0;
(async function() { (async function() {
const fsInventory = JAMBONES_FREESWITCH const fsInventory = process.env.JAMBONES_FREESWITCH
.split(',') .split(',')
.map((fs) => { .map((fs) => {
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs); const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
assert.ok(arr, `Invalid syntax JAMBONES_FREESWITCH: ${JAMBONES_FREESWITCH}`); assert.ok(arr, `Invalid syntax JAMBONES_FREESWITCH: ${process.env.JAMBONES_FREESWITCH}`);
const opts = {address: arr[1], port: arr[2], secret: arr[3]}; const opts = {address: arr[1], port: arr[2], secret: arr[3]};
if (arr.length > 4) opts.advertisedAddress = arr[4]; if (arr.length > 4) opts.advertisedAddress = arr[4];
/* NB: originally for testing only, but for now all jambonz deployments /* NB: originally for testing only, but for now all jambonz deployments
have freeswitch installed locally alongside this app have freeswitch installed locally alongside this app
*/ */
if (NODE_ENV === 'test') opts.listenAddress = '0.0.0.0'; if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
else if (JAMBONES_ESL_LISTEN_ADDRESS) opts.listenAddress = JAMBONES_ESL_LISTEN_ADDRESS; else if (process.env.JAMBONES_ESL_LISTEN_ADDRESS) opts.listenAddress = process.env.JAMBONES_ESL_LISTEN_ADDRESS;
return opts; return opts;
}); });
logger.info({fsInventory}, 'freeswitch inventory'); logger.info({fsInventory}, 'freeswitch inventory');
@@ -141,19 +125,20 @@ function installSrfLocals(srf, logger) {
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
lookupSmppGateways lookupSmppGateways
} = require('@jambonz/db-helpers')({ } = require('@jambonz/db-helpers')({
host: JAMBONES_MYSQL_HOST, host: process.env.JAMBONES_MYSQL_HOST,
user: JAMBONES_MYSQL_USER, user: process.env.JAMBONES_MYSQL_USER,
port: JAMBONES_MYSQL_PORT || 3306, port: process.env.JAMBONES_MYSQL_PORT || 3306,
password: JAMBONES_MYSQL_PASSWORD, password: process.env.JAMBONES_MYSQL_PASSWORD,
database: JAMBONES_MYSQL_DATABASE, database: process.env.JAMBONES_MYSQL_DATABASE,
connectionLimit: JAMBONES_MYSQL_CONNECTION_LIMIT || 10 connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger); }, logger, tracer);
const { const {
client, client,
updateCallStatus, updateCallStatus,
retrieveCall, retrieveCall,
listCalls, listCalls,
deleteCall, deleteCall,
synthAudio,
createHash, createHash,
retrieveHash, retrieveHash,
deleteKey, deleteKey,
@@ -166,32 +151,19 @@ function installSrfLocals(srf, logger) {
pushBack, pushBack,
popFront, popFront,
removeFromList, removeFromList,
getListPosition,
lengthOfList, lengthOfList,
addToSortedSet, getListPosition
retrieveFromSortedSet, } = require('@jambonz/realtimedb-helpers')({
retrieveByPatternSortedSet, host: process.env.JAMBONES_REDIS_HOST,
sortedSetLength, port: process.env.JAMBONES_REDIS_PORT || 6379
sortedSetPositionByPattern
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
host: JAMBONES_REDIS_HOST,
port: JAMBONES_REDIS_PORT || 6379
}, logger, tracer);
const {
synthAudio,
getNuanceAccessToken,
getIbmAccessToken,
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
host: JAMBONES_REDIS_HOST,
port: JAMBONES_REDIS_PORT || 6379
}, logger, tracer); }, logger, tracer);
const { const {
writeAlerts, writeAlerts,
AlertType AlertType
} = require('@jambonz/time-series')(logger, { } = require('@jambonz/time-series')(logger, {
host: JAMBONES_TIME_SERIES_HOST, host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50, commitSize: 50,
commitInterval: 'test' === NODE_ENV ? 7 : 20 commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
}); });
let localIp; let localIp;
@@ -232,19 +204,12 @@ function installSrfLocals(srf, logger) {
popFront, popFront,
removeFromList, removeFromList,
lengthOfList, lengthOfList,
getListPosition, getListPosition
getNuanceAccessToken,
getIbmAccessToken,
addToSortedSet,
retrieveFromSortedSet,
retrieveByPatternSortedSet,
sortedSetLength,
sortedSetPositionByPattern
}, },
parentLogger: logger, parentLogger: logger,
getSBC, getSBC,
getSmpp: () => { getSmpp: () => {
return SMPP_URL; return process.env.SMPP_URL;
}, },
lifecycleEmitter, lifecycleEmitter,
getFreeswitch, getFreeswitch,

View File

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

View File

@@ -4,7 +4,7 @@ const SipError = require('drachtio-srf').SipError;
const {TaskPreconditions, CallDirection} = require('../utils/constants'); const {TaskPreconditions, CallDirection} = require('../utils/constants');
const CallInfo = require('../session/call-info'); const CallInfo = require('../session/call-info');
const assert = require('assert'); const assert = require('assert');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const normalizeJambones = require('../utils/normalize-jambones');
const makeTask = require('../tasks/make_task'); const makeTask = require('../tasks/make_task');
const ConfirmCallSession = require('../session/confirm-call-session'); const ConfirmCallSession = require('../session/confirm-call-session');
const AdultingCallSession = require('../session/adulting-call-session'); const AdultingCallSession = require('../session/adulting-call-session');
@@ -12,7 +12,7 @@ const deepcopy = require('deepcopy');
const moment = require('moment'); const moment = require('moment');
const stripCodecs = require('./strip-ancillary-codecs'); const stripCodecs = require('./strip-ancillary-codecs');
const RootSpan = require('./call-tracer'); const RootSpan = require('./call-tracer');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
class SingleDialer extends Emitter { class SingleDialer extends Emitter {
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) { constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
@@ -45,10 +45,6 @@ class SingleDialer extends Emitter {
return this.callInfo.callStatus; return this.callInfo.callStatus;
} }
get applicationSid() {
return this.application?.application_sid || this.callInfo?.applicationSid;
}
/** /**
* can be used for all http requests within this session * can be used for all http requests within this session
*/ */

View File

@@ -1,9 +1,5 @@
const assert = require('assert'); const assert = require('assert');
const timeSeries = require('@jambonz/time-series'); const timeSeries = require('@jambonz/time-series');
const {
NODE_ENV,
JAMBONES_TIME_SERIES_HOST
} = require('../config');
let alerter ; let alerter ;
function isAbsoluteUrl(u) { function isAbsoluteUrl(u) {
@@ -32,9 +28,9 @@ class Requestor {
if (!alerter) { if (!alerter) {
alerter = timeSeries(logger, { alerter = timeSeries(logger, {
host: JAMBONES_TIME_SERIES_HOST, host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50, commitSize: 50,
commitInterval: 'test' === NODE_ENV ? 7 : 20 commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
}); });
} }
} }
@@ -42,9 +38,9 @@ class Requestor {
get Alerter() { get Alerter() {
if (!alerter) { if (!alerter) {
alerter = timeSeries(this.logger, { alerter = timeSeries(this.logger, {
host: JAMBONES_TIME_SERIES_HOST, host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50, commitSize: 50,
commitInterval: 'test' === NODE_ENV ? 7 : 20 commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
}); });
} }
return alerter; return alerter;

View File

@@ -1,41 +1,31 @@
const assert = require('assert'); const assert = require('assert');
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants'); const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants');
const Emitter = require('events'); const Emitter = require('events');
const debug = require('debug')('jambonz:feature-server'); const debug = require('debug')('jambonz:feature-server');
const noopLogger = {info: () => {}, error: () => {}}; const noopLogger = {info: () => {}, error: () => {}};
const {
JAMBONES_SBCS,
K8S,
K8S_SBC_SIP_SERVICE_NAME,
AWS_SNS_TOPIC_ARM,
OPTIONS_PING_INTERVAL,
AWS_REGION,
NODE_ENV,
JAMBONES_CLUSTER_ID,
} = require('../config');
module.exports = (logger) => { module.exports = (logger) => {
logger = logger || noopLogger; logger = logger || noopLogger;
let idxSbc = 0; let idxSbc = 0;
let sbcs = []; let sbcs = [];
if (JAMBONES_SBCS) { if (process.env.JAMBONES_SBCS) {
sbcs = JAMBONES_SBCS sbcs = process.env.JAMBONES_SBCS
.split(',') .split(',')
.map((sbc) => sbc.trim()); .map((sbc) => sbc.trim());
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured'); assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
logger.info({sbcs}, 'SBC inventory'); logger.info({sbcs}, 'SBC inventory');
} }
else if (K8S && K8S_SBC_SIP_SERVICE_NAME) { else if (process.env.K8S && process.env.K8S_SBC_SIP_SERVICE_NAME) {
sbcs = [`${K8S_SBC_SIP_SERVICE_NAME}:5060`]; sbcs = [`${process.env.K8S_SBC_SIP_SERVICE_NAME}:5060`];
logger.info({sbcs}, 'SBC inventory'); logger.info({sbcs}, 'SBC inventory');
} }
// listen for SNS lifecycle changes // listen for SNS lifecycle changes
let lifecycleEmitter = new Emitter(); let lifecycleEmitter = new Emitter();
let dryUpCalls = false; let dryUpCalls = false;
if (AWS_SNS_TOPIC_ARM && AWS_REGION) { if (process.env.AWS_SNS_TOPIC_ARM && process.env.AWS_REGION) {
(async function() { (async function() {
try { try {
@@ -85,13 +75,9 @@ module.exports = (logger) => {
} }
})(); })();
} }
else if (K8S) {
lifecycleEmitter.scaleIn = () => process.exit(0);
}
async function pingProxies(srf) { async function pingProxies(srf) {
if (NODE_ENV === 'test') return; if (process.env.NODE_ENV === 'test') return;
for (const sbc of sbcs) { for (const sbc of sbcs) {
try { try {
@@ -101,8 +87,7 @@ module.exports = (logger) => {
method: 'OPTIONS', method: 'OPTIONS',
headers: { headers: {
'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed', 'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed',
'X-FS-Calls': srf.locals.sessionTracker.count, 'X-FS-Calls': srf.locals.sessionTracker.count
'X-FS-ServiceUrl': srf.locals.serviceUrl
} }
}); });
req.on('response', (res) => { req.on('response', (res) => {
@@ -113,7 +98,7 @@ module.exports = (logger) => {
} }
} }
} }
if (K8S) { if (process.env.K8S) {
setImmediate(() => { setImmediate(() => {
logger.info('disabling OPTIONS pings since we are running as a kubernetes service'); logger.info('disabling OPTIONS pings since we are running as a kubernetes service');
const {srf} = require('../..'); const {srf} = require('../..');
@@ -134,16 +119,16 @@ module.exports = (logger) => {
setInterval(() => { setInterval(() => {
const {srf} = require('../..'); const {srf} = require('../..');
pingProxies(srf); pingProxies(srf);
}, OPTIONS_PING_INTERVAL); }, process.env.OPTIONS_PING_INTERVAL || 30000);
// initial ping once we are up // initial ping once we are up
setTimeout(async() => { setTimeout(async() => {
// if SBCs are auto-scaling, monitor them as they come and go // if SBCs are auto-scaling, monitor them as they come and go
const {srf} = require('../..'); const {srf} = require('../..');
if (!JAMBONES_SBCS) { if (!process.env.JAMBONES_SBCS) {
const {monitorSet} = srf.locals.dbHelpers; const {monitorSet} = srf.locals.dbHelpers;
const setName = `${(JAMBONES_CLUSTER_ID || 'default')}:active-sip`; const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-sip`;
await monitorSet(setName, 10, (members) => { await monitorSet(setName, 10, (members) => {
sbcs = members; sbcs = members;
logger.info(`sbc-pinger: SBC roster has changed, list of active SBCs is now ${sbcs}`); logger.info(`sbc-pinger: SBC roster has changed, list of active SBCs is now ${sbcs}`);

View File

@@ -1,5 +1,5 @@
const xmlParser = require('xml2js').parseString; const xmlParser = require('xml2js').parseString;
const uuidv4 = require('uuid-random'); const { v4: uuidv4 } = require('uuid');
const parseUri = require('drachtio-srf').parseUri; const parseUri = require('drachtio-srf').parseUri;
const transform = require('sdp-transform'); const transform = require('sdp-transform');
const debug = require('debug')('jambonz:feature-server'); const debug = require('debug')('jambonz:feature-server');
@@ -47,16 +47,8 @@ const parseSiprecPayload = (req, logger) => {
} }
} }
if (!sdp || !meta) {
if (!meta && sdp) { logger.info({payload: req.payload}, 'invalid SIPREC payload');
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')); return reject(new Error('expected multipart SIPREC body'));
} }
@@ -250,8 +242,7 @@ const createSipRecPayload = (sdp1, sdp2, logger) => {
.replace(/a=sendonly\r\n/g, '') .replace(/a=sendonly\r\n/g, '')
.replace(/a=direction:both\r\n/g, ''); .replace(/a=direction:both\r\n/g, '');
*/ */
return combinedSdp;
return combinedSdp.replace(/sendrecv/g, 'recvonly');
}; };
module.exports = { parseSiprecPayload, createSipRecPayload } ; module.exports = { parseSiprecPayload, createSipRecPayload } ;

View File

@@ -1,679 +1,33 @@
const {
TaskName,
AzureTranscriptionEvents,
GoogleTranscriptionEvents,
AwsTranscriptionEvents,
NuanceTranscriptionEvents,
DeepgramTranscriptionEvents,
SonioxTranscriptionEvents,
NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('./constants');
const stickyVars = {
google: [
'GOOGLE_SPEECH_HINTS',
'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL',
'GOOGLE_SPEECH_PROFANITY_FILTER',
'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS',
'GOOGLE_SPEECH_SINGLE_UTTERANCE',
'GOOGLE_SPEECH_SPEAKER_DIARIZATION',
'GOOGLE_SPEECH_USE_ENHANCED',
'GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
'GOOGLE_SPEECH_METADATA_INTERACTION_TYPE',
'GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE'
],
microsoft: [
'AZURE_SPEECH_HINTS',
'AZURE_SERVICE_ENDPOINT_ID',
'AZURE_REQUEST_SNR',
'AZURE_PROFANITY_OPTION',
'AZURE_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 || [])
.map((alt) => ({
confidence: alt.confidence,
transcript: alt.transcript,
}));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [alternatives[0]],
vendor: {
name: 'deepgram',
evt: copy
}
};
};
const normalizeNvidia = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
const alternatives = (evt.alternatives || [])
.map((alt) => ({
confidence: alt.confidence,
transcript: alt.transcript,
}));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives,
vendor: {
name: 'nvidia',
evt: copy
}
};
};
const normalizeIbm = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
//const idx = evt.result_index;
const result = evt.results[0];
return {
language_code: language,
channel_tag: channel,
is_final: result.final,
alternatives: result.alternatives,
vendor: {
name: 'ibm',
evt: copy
}
};
};
const normalizeGoogle = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]],
vendor: {
name: 'google',
evt: copy
}
};
};
const normalizeCustom = (evt, channel, language) => {
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]]
};
};
const normalizeNuance = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]],
vendor: {
name: 'nuance',
evt: copy
}
};
};
const normalizeMicrosoft = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
const nbest = evt.NBest;
const language_code = evt.PrimaryLanguage?.Language || language;
const alternatives = nbest ? nbest.map((n) => {
return {
confidence: n.Confidence,
transcript: n.Display
};
}) :
[
{
transcript: evt.DisplayText || evt.Text
}
];
return {
language_code,
channel_tag: channel,
is_final: evt.RecognitionStatus === 'Success',
alternatives: [alternatives[0]],
vendor: {
name: 'microsoft',
evt: copy
}
};
};
const normalizeAws = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt[0].is_final,
alternatives: evt[0].alternatives,
vendor: {
name: 'aws',
evt: copy
}
};
};
module.exports = (logger) => { module.exports = (logger) => {
const normalizeTranscription = (evt, vendor, channel, language) => { const normalizeTranscription = (evt, vendor, channel) => {
if ('aws' === vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
if ('microsoft' === vendor) {
const nbest = evt.NBest;
const language_code = evt.PrimaryLanguage?.Language || this.language;
const alternatives = nbest ? nbest.map((n) => {
return {
confidence: n.Confidence,
transcript: n.Display
};
}) :
[
{
transcript: evt.DisplayText || evt.Text
}
];
//logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription'); const newEvent = {
switch (vendor) { is_final: evt.RecognitionStatus === 'Success',
case 'deepgram': channel,
return normalizeDeepgram(evt, channel, language); language_code,
case 'microsoft': alternatives
return normalizeMicrosoft(evt, channel, language); };
case 'google': evt = newEvent;
return normalizeGoogle(evt, channel, language);
case 'aws':
return normalizeAws(evt, channel, language);
case 'nuance':
return normalizeNuance(evt, channel, language);
case 'ibm':
return normalizeIbm(evt, channel, language);
case 'nvidia':
return normalizeNvidia(evt, channel, language);
case 'soniox':
return normalizeSoniox(evt, channel, language);
default:
if (vendor.startsWith('custom:')) {
return normalizeCustom(evt, channel, language);
}
logger.error(`Unknown vendor ${vendor}`);
return evt;
} }
evt.channel_tag = channel;
//logger.debug({evt}, 'normalized transcription');
return evt;
}; };
const setChannelVarsForStt = (task, sttCredentials, rOpts = {}) => { return {normalizeTranscription};
let opts = {};
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
const vad = {enable, voiceMs, mode};
const vendor = rOpts.vendor;
/* voice activity detection works across vendors */
opts = {
...opts,
...(vad.enable && {START_RECOGNIZING_ON_VAD: 1}),
...(vad.enable && vad.voiceMs && {RECOGNIZER_VAD_VOICE_MS: vad.voiceMs}),
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
};
if ('google' === vendor) {
const model = task.name === TaskName.Gather ? 'command_and_search' : 'latest_long';
opts = {
...opts,
...(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: 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}),
...(rOpts.altLanguages.length > 0 &&
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
...(rOpts.interactionType &&
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
...{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(vendor)) {
opts = {
...opts,
...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}),
...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}),
...(sttCredentials && {
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
AWS_REGION: sttCredentials.region
}),
};
}
else if ('microsoft' === vendor) {
opts = {
...opts,
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.trim()).join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}),
...(rOpts.altLanguages && rOpts.altLanguages.length > 0 &&
{AZURE_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}),
...(rOpts.initialSpeechTimeoutMs > 0 &&
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
...(sttCredentials && {
AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key,
AZURE_REGION: sttCredentials.region,
}),
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint &&
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint})
};
}
else if ('nuance' === vendor) {
/**
* Note: all nuance options are in recognizer.nuanceOptions, should migrate
* other vendor settings to similar nested structure
*/
const {nuanceOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.access_token) && {NUANCE_ACCESS_TOKEN: sttCredentials.access_token},
...(sttCredentials.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 || rOpts.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
...(nuanceOptions.profanityFilter) &&
{NUANCE_FILTER_PROFANITY: nuanceOptions.profanityFilter},
...(nuanceOptions.includeTokenization) &&
{NUANCE_INCLUDE_TOKENIZATION: nuanceOptions.includeTokenization},
...(nuanceOptions.discardSpeakerAdaptation) &&
{NUANCE_DISCARD_SPEAKER_ADAPTATION: nuanceOptions.discardSpeakerAdaptation},
...(nuanceOptions.suppressCallRecording) &&
{NUANCE_SUPPRESS_CALL_RECORDING: nuanceOptions.suppressCallRecording},
...(nuanceOptions.maskLoadFailures) &&
{NUANCE_MASK_LOAD_FAILURES: nuanceOptions.maskLoadFailures},
...(nuanceOptions.suppressInitialCapitalization) &&
{NUANCE_SUPPRESS_INITIAL_CAPITALIZATION: nuanceOptions.suppressInitialCapitalization},
...(nuanceOptions.allowZeroBaseLmWeight)
&& {NUANCE_ALLOW_ZERO_BASE_LM_WEIGHT: nuanceOptions.allowZeroBaseLmWeight},
...(nuanceOptions.filterWakeupWord) &&
{NUANCE_FILTER_WAKEUP_WORD: nuanceOptions.filterWakeupWord},
...(nuanceOptions.resultType) &&
{NUANCE_RESULT_TYPE: nuanceOptions.resultType || rOpts.interim ? 'partial' : 'final'},
...(nuanceOptions.noInputTimeoutMs) &&
{NUANCE_NO_INPUT_TIMEOUT_MS: nuanceOptions.noInputTimeoutMs},
...(nuanceOptions.recognitionTimeoutMs) &&
{NUANCE_RECOGNITION_TIMEOUT_MS: nuanceOptions.recognitionTimeoutMs},
...(nuanceOptions.utteranceEndSilenceMs) &&
{NUANCE_UTTERANCE_END_SILENCE_MS: nuanceOptions.utteranceEndSilenceMs},
...(nuanceOptions.maxHypotheses) &&
{NUANCE_MAX_HYPOTHESES: nuanceOptions.maxHypotheses},
...(nuanceOptions.speechDomain) &&
{NUANCE_SPEECH_DOMAIN: nuanceOptions.speechDomain},
...(nuanceOptions.formatting) &&
{NUANCE_FORMATTING: nuanceOptions.formatting},
...(nuanceOptions.resources) &&
{NUANCE_RESOURCES: JSON.stringify(nuanceOptions.resources)},
};
}
else if ('deepgram' === vendor) {
const {deepgramOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.api_key) &&
{DEEPGRAM_API_KEY: sttCredentials.api_key},
...(deepgramOptions.tier) &&
{DEEPGRAM_SPEECH_TIER: deepgramOptions.tier},
...(deepgramOptions.model) &&
{DEEPGRAM_SPEECH_MODEL: deepgramOptions.model},
...(deepgramOptions.punctuate) &&
{DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1},
...(deepgramOptions.profanityFilter) &&
{DEEPGRAM_SPEECH_PROFANITY_FILTER: 1},
...(deepgramOptions.redact) &&
{DEEPGRAM_SPEECH_REDACT: 1},
...(deepgramOptions.diarize) &&
{DEEPGRAM_SPEECH_DIARIZE: 1},
...(deepgramOptions.diarizeVersion) &&
{DEEPGRAM_SPEECH_DIARIZE_VERSION: deepgramOptions.diarizeVersion},
...(deepgramOptions.ner) &&
{DEEPGRAM_SPEECH_NER: 1},
...(deepgramOptions.alternatives) &&
{DEEPGRAM_SPEECH_ALTERNATIVES: deepgramOptions.alternatives},
...(deepgramOptions.numerals) &&
{DEEPGRAM_SPEECH_NUMERALS: deepgramOptions.numerals},
...(deepgramOptions.search) &&
{DEEPGRAM_SPEECH_SEARCH: deepgramOptions.search.join(',')},
...(deepgramOptions.replace) &&
{DEEPGRAM_SPEECH_REPLACE: deepgramOptions.replace.join(',')},
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.trim()).join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.phrase).join(',')}),
...(deepgramOptions.keywords) &&
{DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')},
...('endpointing' in deepgramOptions) &&
{DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing},
...(deepgramOptions.vadTurnoff) &&
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
...(deepgramOptions.tag) &&
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
};
}
else if ('soniox' === vendor) {
const {sonioxOptions = {}} = rOpts;
const {storage = {}} = sonioxOptions;
opts = {
...opts,
...(sttCredentials.api_key) &&
{SONIOX_API_KEY: sttCredentials.api_key},
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{SONIOX_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{SONIOX_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' &&
{SONIOX_HINTS_BOOST: rOpts.hintsBoost}),
...(sonioxOptions.model) &&
{SONIOX_MODEL: sonioxOptions.model},
...((sonioxOptions.profanityFilter || rOpts.profanityFilter) && {SONIOX_PROFANITY_FILTER: 1}),
...(storage?.id && {SONIOX_STORAGE_ID: storage.id}),
...(storage?.id && storage?.title && {SONIOX_STORAGE_TITLE: storage.title}),
...(storage?.id && storage?.disableStoreAudio && {SONIOX_STORAGE_DISABLE_AUDIO: 1}),
...(storage?.id && storage?.disableStoreTranscript && {SONIOX_STORAGE_DISABLE_TRANSCRIPT: 1}),
...(storage?.id && storage?.disableSearch && {SONIOX_STORAGE_DISABLE_SEARCH: 1})
};
}
else if ('ibm' === vendor) {
const {ibmOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.access_token) &&
{IBM_ACCESS_TOKEN: sttCredentials.access_token},
...(sttCredentials.stt_region) &&
{IBM_SPEECH_REGION: sttCredentials.stt_region},
...(sttCredentials.instance_id) &&
{IBM_SPEECH_INSTANCE_ID: sttCredentials.instance_id},
...(ibmOptions.model) &&
{IBM_SPEECH_MODEL: ibmOptions.model},
...(ibmOptions.language_customization_id) &&
{IBM_SPEECH_LANGUAGE_CUSTOMIZATION_ID: ibmOptions.language_customization_id},
...(ibmOptions.acoustic_customization_id) &&
{IBM_SPEECH_ACOUSTIC_CUSTOMIZATION_ID: ibmOptions.acoustic_customization_id},
...(ibmOptions.baseModelVersion) &&
{IBM_SPEECH_BASE_MODEL_VERSION: ibmOptions.baseModelVersion},
...(ibmOptions.watsonMetadata) &&
{IBM_SPEECH_WATSON_METADATA: ibmOptions.watsonMetadata},
...(ibmOptions.watsonLearningOptOut) &&
{IBM_SPEECH_WATSON_LEARNING_OPT_OUT: ibmOptions.watsonLearningOptOut}
};
}
else if ('nvidia' === vendor) {
const {nvidiaOptions = {}} = rOpts;
const rivaUri = nvidiaOptions.rivaUri || sttCredentials.riva_server_uri;
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}),
...(rivaUri && {NVIDIA_RIVA_URI: rivaUri}),
...(nvidiaOptions.verbatimTranscripts && {NVIDIA_VERBATIM_TRANSCRIPTS: 1}),
...(rOpts.diarization && {NVIDIA_SPEAKER_DIARIZATION: 1}),
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
{NVIDIA_DIARIZATION_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
...(rOpts.separateRecognitionPerChannel && {NVIDIA_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{NVIDIA_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{NVIDIA_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' &&
{NVIDIA_HINTS_BOOST: rOpts.hintsBoost}),
...(nvidiaOptions.customConfiguration &&
{NVIDIA_CUSTOM_CONFIGURATION: JSON.stringify(nvidiaOptions.customConfiguration)}),
};
}
else if (vendor.startsWith('custom:')) {
let {options = {}} = rOpts;
const {auth_token, custom_stt_url} = sttCredentials;
options = {
...options,
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{hints: rOpts.hints}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{hints: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost})
};
opts = {
...opts,
JAMBONZ_STT_API_KEY: auth_token,
JAMBONZ_STT_URL: custom_stt_url,
...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}),
};
}
(stickyVars[vendor] || []).forEach((key) => {
if (!opts[key]) opts[key] = '';
});
return opts;
};
const removeSpeechListeners = (ep) => {
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
ep.removeCustomEventListener(GoogleTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AwsTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
ep.removeCustomEventListener(AzureTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(NuanceTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NuanceTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Transcription);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Connect);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(SonioxTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Transcription);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Connect);
ep.removeCustomEventListener(JambonzTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Error);
};
const setSpeechCredentialsAtRuntime = (recognizer) => {
if (!recognizer) return;
if (recognizer.vendor === 'nuance') {
const {clientId, secret, kryptonEndpoint} = recognizer.nuanceOptions || {};
if (clientId && secret) return {client_id: clientId, secret};
if (kryptonEndpoint) return {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 {
tts_api_key: ttsApiKey,
tts_region: ttsRegion,
stt_api_key: sttApiKey,
stt_region: sttRegion,
instance_id: instanceId
};
}
};
return {
normalizeTranscription,
setChannelVarsForStt,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
};
}; };

View File

@@ -4,13 +4,9 @@ const short = require('short-uuid');
const {HookMsgTypes} = require('./constants.json'); const {HookMsgTypes} = require('./constants.json');
const Websocket = require('ws'); const Websocket = require('ws');
const snakeCaseKeys = require('./snakecase-keys'); const snakeCaseKeys = require('./snakecase-keys');
const { const HttpRequestor = require('./http-requestor');
RESPONSE_TIMEOUT_MS, const MAX_RECONNECTS = 5;
JAMBONES_WS_PING_INTERVAL_MS, const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
MAX_RECONNECTS,
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
JAMBONES_WS_MAX_PAYLOAD
} = require('../config');
class WsRequestor extends BaseRequestor { class WsRequestor extends BaseRequestor {
constructor(logger, account_sid, hook, secret) { constructor(logger, account_sid, hook, secret) {
@@ -49,7 +45,7 @@ class WsRequestor extends BaseRequestor {
return; return;
} }
if (this.closedGracefully) { if (this.closedGracefully) {
this.logger.debug(`WsRequestor:request - discarding ${type} because socket was closed gracefully`); this.logger.debug(`WsRequestor:request - discarding ${type} because we closed the socket`);
return; return;
} }
@@ -57,10 +53,8 @@ class WsRequestor extends BaseRequestor {
/* if we have an absolute url, and it is http then do a standard webhook */ /* if we have an absolute url, and it is http then do a standard webhook */
if (this._isAbsoluteUrl(url) && url.startsWith('http')) { if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
const HttpRequestor = require('./http-requestor');
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)'); this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
const h = typeof hook === 'object' ? hook : {url: hook}; const requestor = new HttpRequestor(this.logger, this.account_sid, {url: hook}, this.secret);
const requestor = new HttpRequestor(this.logger, this.account_sid, h, this.secret);
if (type === 'session:redirect') { if (type === 'session:redirect') {
this.close(); this.close();
this.emit('handover', requestor); this.emit('handover', requestor);
@@ -101,9 +95,6 @@ class WsRequestor extends BaseRequestor {
assert.ok(url, 'WsRequestor:request url was not provided'); assert.ok(url, 'WsRequestor:request url was not provided');
const msgid = short.generate(); const msgid = short.generate();
// save initial msgid in case we need to reconnect during initial session:new
if (type === 'session:new') this._initMsgId = msgid;
const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {}; const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {};
const obj = { const obj = {
type, type,
@@ -126,18 +117,8 @@ class WsRequestor extends BaseRequestor {
//this.logger.debug({obj}, `websocket: sending (${url})`); //this.logger.debug({obj}, `websocket: sending (${url})`);
/* special case: reconnecting before we received ack to session:new */
let reconnectingWithoutAck = false;
if (type === 'session:reconnect' && this._initMsgId) {
reconnectingWithoutAck = true;
const obj = this.messagesInFlight.get(this._initMsgId);
this.messagesInFlight.delete(this._initMsgId);
this.messagesInFlight.set(msgid, obj);
this._initMsgId = msgid;
}
/* simple notifications */ /* simple notifications */
if (['call:status', 'verb:status', 'jambonz:error'].includes(type) || reconnectingWithoutAck) { if (['call:status', 'jambonz:error', 'session:reconnect'].includes(type)) {
this.ws.send(JSON.stringify(obj), () => { this.ws.send(JSON.stringify(obj), () => {
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`); this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
sendQueuedMsgs(); sendQueuedMsgs();
@@ -149,7 +130,7 @@ class WsRequestor extends BaseRequestor {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
/* give the far end a reasonable amount of time to ack our message */ /* give the far end a reasonable amount of time to ack our message */
const timer = setTimeout(() => { const timer = setTimeout(() => {
const {failure} = this.messagesInFlight.get(msgid) || {}; const {failure} = this.messagesInFlight.get(msgid);
failure && failure(`timeout from far end for msgid ${msgid}`); failure && failure(`timeout from far end for msgid ${msgid}`);
this.messagesInFlight.delete(msgid); this.messagesInFlight.delete(msgid);
}, RESPONSE_TIMEOUT_MS); }, RESPONSE_TIMEOUT_MS);
@@ -179,24 +160,22 @@ class WsRequestor extends BaseRequestor {
}); });
} }
_stopPingTimer() {
if (this._pingTimer) {
clearInterval(this._pingTimer);
this._pingTimer = null;
}
}
close() { close() {
this.closedGracefully = true; this.closedGracefully = true;
this.logger.debug('WsRequestor:close closing socket'); this.logger.debug('WsRequestor:close closing socket');
this._stopPingTimer();
try { try {
if (this.ws) { if (this.ws) {
this.ws.close(1000); this.ws.close();
this.ws.removeAllListeners(); this.ws.removeAllListeners();
this.ws = null;
} }
this._clearPendingMessages();
for (const [msgid, obj] of this.messagesInFlight) {
const {timer} = obj;
clearTimeout(timer);
obj.failure(`abandoning msgid ${msgid} since we have closed the socket`);
}
this.messagesInFlight.clear();
} catch (err) { } catch (err) {
this.logger.info({err}, 'WsRequestor: Error closing socket'); this.logger.info({err}, 'WsRequestor: Error closing socket');
} }
@@ -204,16 +183,15 @@ class WsRequestor extends BaseRequestor {
_connect() { _connect() {
assert(!this.ws); assert(!this.ws);
this._stopPingTimer();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const handshakeTimeout = JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ? const handshakeTimeout = process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ?
parseInt(JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) : parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) :
1500; 1500;
let opts = { let opts = {
followRedirects: true, followRedirects: true,
maxRedirects: 2, maxRedirects: 2,
handshakeTimeout, handshakeTimeout,
maxPayload: JAMBONES_WS_MAX_PAYLOAD ? parseInt(JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024, maxPayload: process.env.JAMBONES_WS_MAX_PAYLOAD ? parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024,
}; };
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`}; if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
@@ -233,6 +211,7 @@ class WsRequestor extends BaseRequestor {
} }
_setHandlers(ws) { _setHandlers(ws) {
this.logger.debug('WsRequestor:_setHandlers');
ws ws
.once('open', this._onOpen.bind(this, ws)) .once('open', this._onOpen.bind(this, ws))
.once('close', this._onClose.bind(this)) .once('close', this._onClose.bind(this))
@@ -241,15 +220,6 @@ class WsRequestor extends BaseRequestor {
.on('error', this._onError.bind(this)); .on('error', this._onError.bind(this));
} }
_clearPendingMessages() {
for (const [msgid, obj] of this.messagesInFlight) {
const {timer} = obj;
clearTimeout(timer);
if (!this._initMsgId) obj.failure(`abandoning msgid ${msgid} since socket is closed`);
}
this.messagesInFlight.clear();
}
_onError(err) { _onError(err) {
if (this.connections > 0) { if (this.connections > 0) {
this.logger.info({url: this.url, err}, 'WsRequestor:_onError'); this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
@@ -265,15 +235,10 @@ class WsRequestor extends BaseRequestor {
this.connectInProgress = false; this.connectInProgress = false;
this.connections++; this.connections++;
this.emit('ready', ws); this.emit('ready', ws);
if (JAMBONES_WS_PING_INTERVAL_MS > 15000) {
this._pingTimer = setInterval(() => this.ws?.ping(), JAMBONES_WS_PING_INTERVAL_MS);
}
} }
_onClose(code) { _onClose(code) {
this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`); this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`);
this._stopPingTimer();
if (this.connections > 0 && code !== 1000) { if (this.connections > 0 && code !== 1000) {
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side'); this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
this.emit('socket-closed'); this.emit('socket-closed');
@@ -292,15 +257,12 @@ class WsRequestor extends BaseRequestor {
}, 'WsRequestor - unexpected response'); }, 'WsRequestor - unexpected response');
this.emit('connection-failure'); this.emit('connection-failure');
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`)); this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
this.connections++;
} }
_onSocketClosed() { _onSocketClosed() {
this.ws = null; this.ws = null;
this.emit('connection-dropped'); this.emit('connection-dropped');
this._stopPingTimer();
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) { if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
if (!this._initMsgId) this._clearPendingMessages();
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`); this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
setTimeout(() => { setTimeout(() => {
this.logger.debug( this.logger.debug(
@@ -352,13 +314,12 @@ class WsRequestor extends BaseRequestor {
} }
_recvAck(msgid, data) { _recvAck(msgid, data) {
this._initMsgId = null;
const obj = this.messagesInFlight.get(msgid); const obj = this.messagesInFlight.get(msgid);
if (!obj) { if (!obj) {
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`); this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
return; return;
} }
//this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`); this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
this.messagesInFlight.delete(msgid); this.messagesInFlight.delete(msgid);
const {success} = obj; const {success} = obj;
success && success(data); success && success(data);

7398
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "0.8.3", "version": "v0.7.7",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 10.16.0" "node": ">= 10.16.0"
@@ -19,57 +19,52 @@
"bugs": {}, "bugs": {},
"scripts": { "scripts": {
"start": "node app", "start": "node app",
"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/ ", "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:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js tracer.js lib", "jslint": "eslint app.js lib"
"jslint:fix": "eslint app.js tracer.js lib --fix"
}, },
"dependencies": { "dependencies": {
"@jambonz/db-helpers": "^0.9.0",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.8.6", "@jambonz/db-helpers": "^0.7.3",
"@jambonz/speech-utils": "^0.0.15", "@jambonz/realtimedb-helpers": "^0.5.9",
"@jambonz/stats-collector": "^0.1.8", "@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.2.7", "@jambonz/time-series": "^0.2.5",
"@jambonz/verb-specifications": "^0.0.25", "@opentelemetry/api": "^1.1.0",
"@opentelemetry/api": "^1.4.0", "@opentelemetry/exporter-jaeger": "^1.3.1",
"@opentelemetry/exporter-jaeger": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0", "@opentelemetry/exporter-zipkin": "^1.3.1",
"@opentelemetry/exporter-zipkin": "^1.9.0", "@opentelemetry/instrumentation": "^0.27.0",
"@opentelemetry/instrumentation": "^0.35.0", "@opentelemetry/resources": "^1.3.1",
"@opentelemetry/resources": "^1.9.0", "@opentelemetry/sdk-trace-base": "^1.3.1",
"@opentelemetry/sdk-trace-base": "^1.9.0", "@opentelemetry/sdk-trace-node": "^1.3.1",
"@opentelemetry/sdk-trace-node": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.3.1",
"@opentelemetry/semantic-conventions": "^1.9.0", "aws-sdk": "^2.1152.0",
"aws-sdk": "^2.1313.0",
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "debug": "^4.3.4",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.23", "drachtio-fsmrf": "^3.0.8",
"drachtio-srf": "^4.5.26", "drachtio-srf": "^4.5.18",
"express": "^4.18.2", "express": "^4.18.1",
"helmet": "^5.1.0",
"ip": "^1.1.8", "ip": "^1.1.8",
"moment": "^2.29.4", "moment": "^2.29.4",
"parse-url": "^8.1.0", "parse-url": "^8.1.0",
"pino": "^8.8.0", "pino": "^6.14.0",
"polly-ssml-split": "^0.1.0",
"proxyquire": "^2.1.3",
"sdp-transform": "^2.14.1", "sdp-transform": "^2.14.1",
"short-uuid": "^4.2.2", "short-uuid": "^4.2.0",
"sinon": "^15.0.1",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^5.19.1", "undici": "^5.8.2",
"uuid-random": "^1.3.2", "uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.1.0", "verify-aws-sns-signature": "^0.1.0",
"ws": "^8.9.0", "ws": "^8.8.0",
"xml2js": "^0.5.0" "xml2js": "^0.4.23"
}, },
"devDependencies": { "devDependencies": {
"clear-module": "^4.1.2", "clear-module": "^4.1.2",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-promise": "^4.3.1", "eslint-plugin-promise": "^4.3.1",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"tape": "^5.6.1" "tape": "^5.5.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"bufferutil": "^4.0.6", "bufferutil": "^4.0.6",

View File

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

View File

@@ -92,7 +92,7 @@ test('test create-call call-hook basic authentication', async(t) => {
"text": "hello" "text": "hello"
} }
]; ];
await provisionCallHook(from, verbs); provisionCallHook(from, verbs);
//THEN //THEN
await p; await p;
@@ -106,117 +106,3 @@ test('test create-call call-hook basic authentication', async(t) => {
t.error(err); t.error(err);
} }
}); });
test('test create-call amd', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let from = 'create-call-amd';
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
// Give UAS app time to come up
const p = sippUac('uas.xml', '172.38.0.10', from);
await waitFor(1000);
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
"call_hook": {
"url": "http://127.0.0.1:3100/",
"method": "POST",
"username": "username",
"password": "password"
},
"from": from,
"to": {
"type": "phone",
"number": "15583084809"
},
"amd": {
"actionHook": "/actionHook"
},
"speech_recognizer_vendor": "google",
"speech_recognizer_language": "en"
});
let verbs = [
{
"verb": "pause",
"length": 7
}
];
await provisionCallHook(from, verbs);
//THEN
await p;
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`)
t.ok(obj.body.type = 'amd_no_speech_detected',
'create-call: AMD detected');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('test create-call app_json', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let from = 'create-call-app-json';
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
// Give UAS app time to come up
const p = sippUac('uas.xml', '172.38.0.10', from);
await waitFor(1000);
const app_json = `[
{
"verb": "pause",
"length": 7
}
]`;
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
"call_hook": {
"url": "http://127.0.0.1:3100/",
"method": "POST",
"username": "username",
"password": "password"
},
app_json,
"from": from,
"to": {
"type": "phone",
"number": "15583084809"
},
"amd": {
"actionHook": "/actionHook"
},
"speech_recognizer_vendor": "google",
"speech_recognizer_language": "en"
});
//THEN
await p;
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -2,14 +2,6 @@ const test = require('tape') ;
const exec = require('child_process').exec ; const exec = require('child_process').exec ;
const fs = require('fs'); const fs = require('fs');
const {encrypt} = require('../lib/utils/encrypt-decrypt'); const {encrypt} = require('../lib/utils/encrypt-decrypt');
const {
GCP_JSON_KEY,
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_REGION,
MICROSOFT_REGION,
MICROSOFT_API_KEY,
} = require('../lib/config');
test('creating jambones_test database', (t) => { test('creating jambones_test database', (t) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => { exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
@@ -26,38 +18,24 @@ test('creating schema', (t) => {
if (err) return t.end(err); if (err) return t.end(err);
t.pass('schema and test data successfully created'); t.pass('schema and test data successfully created');
const sql = []; if (process.env.GCP_JSON_KEY && process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
if (GCP_JSON_KEY) { const google_credential = encrypt(process.env.GCP_JSON_KEY);
const google_credential = encrypt(GCP_JSON_KEY);
t.pass('adding google credentials');
sql.push(`UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';`);
}
if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) {
const aws_credential = encrypt(JSON.stringify({ const aws_credential = encrypt(JSON.stringify({
access_key_id: AWS_ACCESS_KEY_ID, access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: AWS_SECRET_ACCESS_KEY, secret_access_key: process.env.AWS_SECRET_ACCESS_KEY
aws_region: AWS_REGION
})); }));
t.pass('adding aws credentials'); const cmd = `
sql.push(`UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';`); UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';
} UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';
if (MICROSOFT_REGION && MICROSOFT_API_KEY) { `;
const microsoft_credential = encrypt(JSON.stringify({
region: MICROSOFT_REGION,
api_key: MICROSOFT_API_KEY
}));
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`; const path = `${__dirname}/.creds.sql`;
const cmd = sql.join('\n'); fs.writeFileSync(path, cmd);
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) => { 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(stdout);
console.log(stderr); console.log(stderr);
if (err) return t.end(err); if (err) return t.end(err);
fs.unlinkSync(path) fs.unlinkSync(path)
fs.writeFileSync(`${__dirname}/credentials/gcp.json`, process.env.GCP_JSON_KEY);
t.pass('set account-level speech credentials'); t.pass('set account-level speech credentials');
t.end(); t.end();
}); });

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
/* SQLEditor (MySQL (2))*/ /* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0; SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips; DROP TABLE IF EXISTS account_static_ips;
DROP TABLE IF EXISTS account_limits;
DROP TABLE IF EXISTS account_products; DROP TABLE IF EXISTS account_products;
DROP TABLE IF EXISTS account_subscriptions; DROP TABLE IF EXISTS account_subscriptions;
@@ -15,18 +14,10 @@ DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS dns_records; DROP TABLE IF EXISTS dns_records;
DROP TABLE IF EXISTS lcr;
DROP TABLE IF EXISTS lcr_carrier_set_entry; DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes; DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS password_settings;
DROP TABLE IF EXISTS user_permissions;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS predefined_sip_gateways; DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways; DROP TABLE IF EXISTS predefined_smpp_gateways;
@@ -45,16 +36,12 @@ DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS ms_teams_tenants; DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS service_provider_limits;
DROP TABLE IF EXISTS signup_history; DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses; DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS speech_credentials; DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS system_information;
DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS smpp_gateways; DROP TABLE IF EXISTS smpp_gateways;
@@ -82,15 +69,6 @@ private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
PRIMARY KEY (account_static_ip_sid) PRIMARY KEY (account_static_ip_sid)
); );
CREATE TABLE account_limits
(
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
);
CREATE TABLE account_subscriptions CREATE TABLE account_subscriptions
( (
account_subscription_sid CHAR(36) NOT NULL UNIQUE , account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
@@ -139,38 +117,11 @@ PRIMARY KEY (dns_record_sid)
CREATE TABLE lcr_routes CREATE TABLE lcr_routes
( (
lcr_route_sid CHAR(36), lcr_route_sid CHAR(36),
lcr_sid CHAR(36) NOT NULL,
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls', regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024), description VARCHAR(1024),
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first', priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid) PRIMARY KEY (lcr_route_sid)
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches'; ) COMMENT='Least cost routing table';
CREATE TABLE lcr
(
lcr_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
is_active BOOLEAN NOT NULL DEFAULT 1,
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
service_provider_sid CHAR(36),
account_sid CHAR(36),
PRIMARY KEY (lcr_sid)
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
CREATE TABLE password_settings
(
min_password_length INTEGER NOT NULL DEFAULT 8,
require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
);
CREATE TABLE predefined_carriers CREATE TABLE predefined_carriers
( (
@@ -263,10 +214,7 @@ CREATE TABLE sbc_addresses
sbc_address_sid CHAR(36) NOT NULL UNIQUE , sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL, ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060, port INTEGER NOT NULL DEFAULT 5060,
tls_port INTEGER,
wss_port INTEGER,
service_provider_sid CHAR(36), service_provider_sid CHAR(36),
last_updated DATETIME,
PRIMARY KEY (sbc_address_sid) PRIMARY KEY (sbc_address_sid)
); );
@@ -280,15 +228,6 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid) PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant'; ) COMMENT='A Microsoft Teams customer tenant';
CREATE TABLE service_provider_limits
(
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
);
CREATE TABLE signup_history CREATE TABLE signup_history
( (
email VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL,
@@ -325,13 +264,6 @@ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (speech_credential_sid) PRIMARY KEY (speech_credential_sid)
); );
CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255)
);
CREATE TABLE users CREATE TABLE users
( (
user_sid CHAR(36) NOT NULL UNIQUE , user_sid CHAR(36) NOT NULL UNIQUE ,
@@ -351,7 +283,6 @@ email_activation_code VARCHAR(16),
email_validated BOOLEAN NOT NULL DEFAULT false, email_validated BOOLEAN NOT NULL DEFAULT false,
phone_validated BOOLEAN NOT NULL DEFAULT false, phone_validated BOOLEAN NOT NULL DEFAULT false,
email_content_opt_out BOOLEAN NOT NULL DEFAULT false, email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (user_sid) PRIMARY KEY (user_sid)
); );
@@ -379,21 +310,9 @@ smpp_password VARCHAR(64),
smpp_enquire_link_interval INTEGER DEFAULT 0, smpp_enquire_link_interval INTEGER DEFAULT 0,
smpp_inbound_system_id VARCHAR(255), smpp_inbound_system_id VARCHAR(255),
smpp_inbound_password VARCHAR(64), smpp_inbound_password VARCHAR(64),
register_from_user VARCHAR(128),
register_from_domain VARCHAR(255),
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
register_status VARCHAR(4096),
PRIMARY KEY (voip_carrier_sid) PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls'; ) COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
);
CREATE TABLE smpp_gateways CREATE TABLE smpp_gateways
( (
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE , smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
@@ -411,7 +330,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers CREATE TABLE phone_numbers
( (
phone_number_sid CHAR(36) UNIQUE , phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL UNIQUE , number VARCHAR(32) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36), voip_carrier_sid CHAR(36),
account_sid CHAR(36), account_sid CHAR(36),
application_sid CHAR(36), application_sid CHAR(36),
@@ -429,7 +348,6 @@ inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound ca
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN', outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL, voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT 1,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid) PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination'; ) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -462,7 +380,6 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ', call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events', call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ', messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google', speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US', speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64), speech_synthesis_voice VARCHAR(64),
@@ -501,11 +418,6 @@ disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
trial_end_date DATETIME, trial_end_date DATETIME,
deactivated_reason VARCHAR(255), deactivated_reason VARCHAR(255),
device_to_call_ratio INTEGER NOT NULL DEFAULT 5, device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
subspace_client_id VARCHAR(255),
subspace_client_secret VARCHAR(255),
subspace_sip_teleport_id VARCHAR(255),
subspace_sip_teleport_destinations VARCHAR(255),
siprec_hook_sid CHAR(36),
PRIMARY KEY (account_sid) PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services'; ) COMMENT='An enterprise that uses the platform for comm services';
@@ -513,31 +425,19 @@ CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_
CREATE INDEX account_sid_idx ON account_static_ips (account_sid); CREATE INDEX account_sid_idx ON account_static_ips (account_sid);
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid); ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX account_sid_idx ON account_limits (account_sid);
ALTER TABLE account_limits ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid) ON DELETE CASCADE;
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid); CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid); CREATE INDEX account_sid_idx ON account_subscriptions (account_sid);
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code); CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid); CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid); ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid); CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid);
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
CREATE INDEX account_sid_idx ON lcr (account_sid);
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid); CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid); CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid); CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
@@ -556,14 +456,14 @@ ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REF
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid); CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
CREATE INDEX account_sid_idx ON account_offers (account_sid); CREATE INDEX account_sid_idx ON account_offers (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX product_sid_idx ON account_offers (product_sid); CREATE INDEX product_sid_idx ON account_offers (product_sid);
ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid); ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid);
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid); CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
CREATE INDEX account_sid_idx ON api_keys (account_sid); CREATE INDEX account_sid_idx ON api_keys (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid); CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -577,53 +477,44 @@ ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid); CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn); CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
CREATE INDEX service_provider_sid_idx ON service_provider_limits (service_provider_sid);
ALTER TABLE service_provider_limits ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid) ON DELETE CASCADE;
CREATE INDEX email_idx ON signup_history (email); CREATE INDEX email_idx ON signup_history (email);
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid); CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid); CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid); CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid); CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid); CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON speech_credentials (account_sid); CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX user_sid_idx ON users (user_sid); CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email); CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone); CREATE INDEX phone_idx ON users (phone);
CREATE INDEX account_sid_idx ON users (account_sid); CREATE INDEX account_sid_idx ON users (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid); CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX email_activation_code_idx ON users (email_activation_code); CREATE INDEX email_activation_code_idx ON users (email_activation_code);
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
CREATE INDEX account_sid_idx ON voip_carriers (account_sid); CREATE INDEX account_sid_idx ON voip_carriers (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid); CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid); CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
@@ -633,12 +524,12 @@ CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid); CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port); CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
@@ -654,10 +545,10 @@ CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
CREATE INDEX application_sid_idx ON applications (application_sid); CREATE INDEX application_sid_idx ON applications (application_sid);
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid); CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON applications (account_sid); CREATE INDEX account_sid_idx ON applications (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_12 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid); ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -673,7 +564,7 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
CREATE INDEX account_sid_idx ON accounts (account_sid); CREATE INDEX account_sid_idx ON accounts (account_sid);
CREATE INDEX sip_realm_idx ON accounts (sip_realm); CREATE INDEX sip_realm_idx ON accounts (sip_realm);
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid); CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_10 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid); ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -681,6 +572,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid); ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid); SET FOREIGN_KEY_CHECKS=0;
SET FOREIGN_KEY_CHECKS=1;

View File

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

View File

@@ -44,7 +44,7 @@ services:
drachtio: drachtio:
image: drachtio/drachtio-server:latest image: drachtio/drachtio-server:latest
restart: always restart: always
command: drachtio --contact "sip:*;transport=udp" --mtu 4096 --address 0.0.0.0 --port 9022 command: drachtio --contact "sip:*;transport=udp,tcp" --address 0.0.0.0 --port 9022
ports: ports:
- "9060:9022/tcp" - "9060:9022/tcp"
networks: networks:
@@ -57,7 +57,7 @@ services:
condition: service_healthy condition: service_healthy
freeswitch: freeswitch:
image: drachtio/drachtio-freeswitch-mrf:0.4.18 image: drachtio/drachtio-freeswitch-mrf:v1.10.1-full
restart: always restart: always
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100 command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
environment: environment:
@@ -68,7 +68,7 @@ services:
- /tmp:/tmp - /tmp:/tmp
- ./credentials:/opt/credentials - ./credentials:/opt/credentials
healthcheck: healthcheck:
test: ['CMD', 'fs_cli' ,'-p', 'JambonzR0ck$$', '-x', '"sofia status"'] test: ['CMD', 'fs_cli' ,'-x', '"sofia status"']
timeout: 5s timeout: 5s
retries: 15 retries: 15
networks: networks:

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ test('\'play\' tests single link in plain text', async(t) => {
]; ];
const from = 'play_single_link'; const from = 'play_single_link';
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
// THEN // THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from); await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
@@ -62,7 +62,7 @@ test('\'play\' tests multi links in array', async(t) => {
]; ];
const from = 'play_multi_links_in_array'; const from = 'play_multi_links_in_array';
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
// THEN // THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from); await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
@@ -100,8 +100,8 @@ test('\'play\' tests single link in conference', async(t) => {
waitHook: `/customHook` waitHook: `/customHook`
} }
]; ];
await provisionCustomHook(from, waitHookVerbs) provisionCustomHook(from, waitHookVerbs)
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
// THEN // THEN
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from); await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
@@ -141,8 +141,8 @@ test('\'play\' tests multi links in array in conference', async(t) => {
waitHook: `/customHook` waitHook: `/customHook`
} }
]; ];
await provisionCustomHook(from, waitHookVerbs) provisionCustomHook(from, waitHookVerbs)
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
// THEN // THEN
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from); await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
@@ -178,73 +178,17 @@ test('\'play\' tests with seekOffset and actionHook', async(t) => {
const waitHookVerbs = []; const waitHookVerbs = [];
const from = 'play_action_hook'; const from = 'play_action_hook';
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
await provisionCustomHook(from, waitHookVerbs) provisionCustomHook(from, waitHookVerbs)
// THEN // THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from); await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds'); t.pass('play: succeeds');
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`); const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
const seconds = parseInt(obj.body.playback_seconds); t.ok(obj.body.reason === "playCompleted", "play: actionHook success received")
const milliseconds = parseInt(obj.body.playback_milliseconds); t.ok(obj.body.playback_seconds === "2", "playback_seconds: actionHook success received")
const lastOffsetPos = parseInt(obj.body.playback_last_offset_pos); t.ok(obj.body.playback_milliseconds === "2048", "playback_milliseconds: actionHook success received")
//console.log({obj}, 'lastRequest'); t.ok(obj.body.playback_last_offset_pos === "16000", "playback_last_offset_pos: actionHook success received")
t.ok(obj.body.reason === "playCompleted", "play: actionHook success received");
t.ok(seconds === 2, "playback_seconds: actionHook success received");
t.ok(milliseconds === 2048, "playback_milliseconds: actionHook success received");
t.ok(lastOffsetPos > 15500 && lastOffsetPos < 16500, "playback_last_offset_pos: actionHook success received")
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'play\' tests with earlymedia', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'silence_stream://5000',
earlyMedia: true
}
];
const from = 'play_early_media';
await 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(); disconnect();
} catch (err) { } catch (err) {
console.log(`error received: ${err}`); console.log(`error received: ${err}`);

View File

@@ -1,127 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook, provisionActionHook, provisionAnyHook} = require('./utils');
const bent = require('bent');
const getJSON = bent('json');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
test('\'enqueue-dequeue\' tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'enqueue',
name: 'support',
actionHook: '/actionHook'
}
];
const verbs2 = [
{
verb: 'dequeue',
name: 'support'
}
];
const actionVerbs = [
{
verb: 'play',
url: 'silence_stream://1000',
earlyMedia: true
}
];
const from = 'enqueue_success';
await provisionCallHook(from, verbs);
await provisionActionHook(from, actionVerbs)
const from2 = 'dequeue_success';
await provisionCallHook(from2, verbs2);
// THEN
const p1 = sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
await sleepFor(1000);
const p2 = sippUac('uac-success-send-bye.xml', '172.38.0.11', from2);
await Promise.all([p1, p2]);
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj.body.queue_result === 'bridged');
t.pass('enqueue-dequeue: succeeds connect');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\leave\' tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'enqueue',
name: 'support1',
waitHook: '/anyHook/enqueue_success_leave',
actionHook: '/actionHook'
}
];
const anyHookVerbs = [
{
verb: 'leave'
}
];
const actionVerbs = [
{
verb: 'play',
url: 'silence_stream://1000',
earlyMedia: true
}
];
const from = 'enqueue_success_leave';
await provisionCallHook(from, verbs);
await provisionAnyHook(from, anyHookVerbs);
await provisionActionHook(from, actionVerbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/enqueue_success_leave`);
t.ok(obj.body.queue_position === 0);
const obj1 = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
t.ok(obj1.body.queue_result === 'leave');
t.pass('enqueue-dequeue: succeeds connect');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -5,6 +5,7 @@ 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) => { 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); if (err) return t.end(err);
t.pass('database successfully dropped'); t.pass('database successfully dropped');
fs.unlinkSync(`${__dirname}/credentials/gcp.json`);
t.end(); t.end();
}); });
}); });

View File

@@ -31,7 +31,7 @@ test('\'say\' tests', async(t) => {
]; ];
const from = 'say_test_success'; const from = 'say_test_success';
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
// THEN // THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from); await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
@@ -43,84 +43,3 @@ test('\'say\' tests', async(t) => {
t.error(err); t.error(err);
} }
}); });
test('\'config\' reset synthesizer tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
"verb": "config",
"synthesizer": {
"vendor": "microsft",
"voice": "foobar"
},
},
{
"verb": "config",
"reset": 'synthesizer',
},
{
verb: 'say',
text: 'hello'
}
];
const from = 'say_test_success';
await provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('say: succeeds when using using account credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
const {MICROSOFT_CUSTOM_API_KEY, MICROSOFT_DEPLOYMENT_ID, MICROSOFT_CUSTOM_REGION, MICROSOFT_CUSTOM_VOICE} = process.env;
if (MICROSOFT_CUSTOM_API_KEY && MICROSOFT_DEPLOYMENT_ID && MICROSOFT_CUSTOM_REGION && MICROSOFT_CUSTOM_VOICE) {
test('\'say\' tests - microsoft custom voice', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'say',
text: 'hello',
synthesizer: {
vendor: 'microsoft',
voice: MICROSOFT_CUSTOM_VOICE,
options: {
deploymentId: MICROSOFT_DEPLOYMENT_ID,
apiKey: MICROSOFT_CUSTOM_API_KEY,
region: MICROSOFT_CUSTOM_REGION,
}
}
}
];
const from = 'say_test_success';
await provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('say: succeeds when using microsoft custom voice');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ test('sending SIP in-dialog requests tests', async(t) => {
} }
]; ];
let from = "sip_indialog_test"; let from = "sip_indialog_test";
await provisionCallHook(from, verbs); provisionCallHook(from, verbs);
// THEN // THEN
await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10', from); await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10', from);
const obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`); const obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);

View File

@@ -53,13 +53,6 @@ test('incoming call tests', (t) => {
.then(() => { .then(() => {
return t.pass('handles in-dialog requests'); return t.pass('handles in-dialog requests');
}) })
.then(() => {
return sippUac('uac-refer-no-notify.xml', '172.38.0.30');
})
.then(() => {
return t.pass('handles sip:refer where we get 202 but no NOTIFY');
})
.then(() => { .then(() => {
srf.disconnect(); srf.disconnect();
t.end(); t.end();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,55 +1,14 @@
const assert = require('assert');
const fs = require('fs');
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const Websocket = require('ws');
const listenPort = process.env.HTTP_PORT || 3000; const listenPort = process.env.HTTP_PORT || 3000;
const any_hook_json_mapping = new Map();
let json_mapping = new Map(); let json_mapping = new Map();
let hook_mapping = new Map(); let hook_mapping = new Map();
let ws_packet_count = new Map();
let ws_metadata = new Map();
/** websocket server for listen audio */ app.listen(listenPort, () => {
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}`); 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.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
@@ -61,8 +20,7 @@ app.use(express.json());
app.all('/', (req, res) => { app.all('/', (req, res) => {
console.log(req.body, 'POST /'); console.log(req.body, 'POST /');
const key = req.body.from const key = req.body.from
addRequestToMap(key, req, hook_mapping); return getJsonFromMap(key, req, res);
return getJsonFromMap(json_mapping, key, req, res);
}); });
app.post('/appMapping', (req, res) => { app.post('/appMapping', (req, res) => {
@@ -81,16 +39,7 @@ app.post('/callStatus', (req, res) => {
return res.sendStatus(200); return res.sendStatus(200);
}); });
/* /*
* transcriptionHook * action Hook
*/
app.post('/transcriptionHook', (req, res) => {
console.log({payload: req.body}, 'POST /transcriptionHook');
let key = req.body.from + "_actionHook"
addRequestToMap(key, req, hook_mapping);
return res.json([{"verb": "hangup"}]);
});
/*
* actionHook
*/ */
app.post('/actionHook', (req, res) => { app.post('/actionHook', (req, res) => {
console.log({payload: req.body}, 'POST /actionHook'); console.log({payload: req.body}, 'POST /actionHook');
@@ -107,7 +56,7 @@ app.post('/actionHook', (req, res) => {
app.all('/customHook', (req, res) => { app.all('/customHook', (req, res) => {
let key = `${req.body.from}_customHook`;; let key = `${req.body.from}_customHook`;;
console.log(req.body, `POST /customHook`); console.log(req.body, `POST /customHook`);
return getJsonFromMap(json_mapping, key, req, res); return getJsonFromMap(key, req, res);
}); });
app.post('/customHookMapping', (req, res) => { app.post('/customHookMapping', (req, res) => {
@@ -117,23 +66,6 @@ app.post('/customHookMapping', (req, res) => {
return res.sendStatus(200); return res.sendStatus(200);
}); });
/**
* Any Hook
*/
app.all('/anyHook/:key', (req, res) => {
let key = req.params.key;
console.log(req.body, `POST /anyHook/${key}`);
return getJsonFromMap(any_hook_json_mapping, key, req, res);
});
app.post('/anyHookMapping', (req, res) => {
let key = req.body.key;
console.log(req.body, `POST /anyHookMapping/${key}`);
any_hook_json_mapping.set(key, req.body.data);
return res.sendStatus(200);
});
// Fetch Requests // Fetch Requests
app.get('/requests/:key', (req, res) => { app.get('/requests/:key', (req, res) => {
let key = req.params.key; let key = req.params.key;
@@ -155,34 +87,13 @@ 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 * private function
*/ */
function getJsonFromMap(map, key, req, res) { function getJsonFromMap(key, req, res) {
if (!map.has(key)) return res.sendStatus(404); if (!json_mapping.has(key)) return res.sendStatus(404);
const retData = JSON.parse(map.get(key)); const retData = JSON.parse(json_mapping.get(key));
console.log(retData, ` Response to ${req.method} ${req.url}`); console.log(retData, ` Response to ${req.method} ${req.url}`);
addRequestToMap(key, req, hook_mapping); addRequestToMap(key, req, hook_mapping);
return res.json(retData); return res.json(retData);

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,17 +2,13 @@ const test = require('tape');
const { sippUac } = require('./sipp')('test_fs'); const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module'); const clearModule = require('clear-module');
const {provisionCallHook} = require('./utils') const {provisionCallHook} = require('./utils')
const {
JAMBONES_LOGLEVEL,
JAMBONES_TIME_SERIES_HOST
} = require('../lib/config');
const opts = { const opts = {
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}, timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
level: JAMBONES_LOGLEVEL level: process.env.JAMBONES_LOGLEVEL || 'info'
}; };
const logger = require('pino')(opts); const logger = require('pino')(opts);
const { queryAlerts } = require('@jambonz/time-series')( const { queryAlerts } = require('@jambonz/time-series')(
logger, JAMBONES_TIME_SERIES_HOST logger, process.env.JAMBONES_TIME_SERIES_HOST
); );
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
@@ -45,7 +41,7 @@ test('basic webhook tests', async(t) => {
]; ];
const from = 'sip_decline_test_success'; const from = 'sip_decline_test_success';
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
await sippUac('uac-expect-603.xml', '172.38.0.10', from); await sippUac('uac-expect-603.xml', '172.38.0.10', from);
t.pass('webhook successfully declines call'); t.pass('webhook successfully declines call');
@@ -73,7 +69,7 @@ test('invalid jambonz json create alert tests', async(t) => {
}; };
const from = 'invalid_json_create_alert'; const from = 'invalid_json_create_alert';
await provisionCallHook(from, verbs) provisionCallHook(from, verbs)
// THEN // THEN
await sippUac('uac-invite-expect-480.xml', '172.38.0.10', from); await sippUac('uac-invite-expect-480.xml', '172.38.0.10', from);

View File

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

View File

@@ -1,201 +0,0 @@
const test = require('tape');
const sinon = require('sinon');
const proxyquire = require("proxyquire");
proxyquire.noCallThru();
const MockWebsocket = require('./ws-mock')
const {
JAMBONES_LOGLEVEL,
} = require('../lib/config');
const logger = require('pino')({level: JAMBONES_LOGLEVEL});
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();
}
});

View File

@@ -7,16 +7,12 @@ const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
const { OTLPTraceExporter } = require ('@opentelemetry/exporter-trace-otlp-http'); const { OTLPTraceExporter } = require ('@opentelemetry/exporter-trace-otlp-http');
const { //const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
JAMBONES_OTEL_ENABLED, //const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
OTEL_EXPORTER_JAEGER_AGENT_HOST, //const { PinoInstrumentation } = require('@opentelemetry/instrumentation-pino');
OTEL_EXPORTER_JAEGER_ENDPOINT,
OTEL_EXPORTER_ZIPKIN_URL,
OTEL_EXPORTER_COLLECTOR_URL
} = require('./lib/config');
module.exports = (serviceName) => { module.exports = (serviceName) => {
if (JAMBONES_OTEL_ENABLED) { if (process.env.JAMBONES_OTEL_ENABLED) {
const {version} = require('./package.json'); const {version} = require('./package.json');
const provider = new NodeTracerProvider({ const provider = new NodeTracerProvider({
resource: new Resource({ resource: new Resource({
@@ -26,15 +22,15 @@ module.exports = (serviceName) => {
}); });
let exporter; let exporter;
if (OTEL_EXPORTER_JAEGER_AGENT_HOST || OTEL_EXPORTER_JAEGER_ENDPOINT) { if (process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST) {
exporter = new JaegerExporter(); exporter = new JaegerExporter();
} }
else if (OTEL_EXPORTER_ZIPKIN_URL) { else if (process.env.OTEL_EXPORTER_ZIPKIN_URL) {
exporter = new ZipkinExporter({url:OTEL_EXPORTER_ZIPKIN_URL}); exporter = new ZipkinExporter({url:process.env.OTEL_EXPORTER_ZIPKIN_URL});
} }
else { else {
exporter = new OTLPTraceExporter({ exporter = new OTLPTraceExporter({
url: OTEL_EXPORTER_COLLECTOR_URL url: process.OTEL_EXPORTER_COLLECTOR_URL
}); });
} }