mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-15 10:49:07 +00:00
Compare commits
1 Commits
v0.7.6-rc1
...
snyk-upgra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08f63532c5 |
@@ -1,10 +1,10 @@
|
||||
FROM node:lts-slim
|
||||
FROM node:17.4-slim
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm prune
|
||||
COPY . /opt/app
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
This application implements the core feature server of the jambones platform.
|
||||
|
||||
> Note: If you are a developer looking to work on the code please read our [how-to for that](./docs/contributing.md).
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is provided via environment variables:
|
||||
@@ -86,5 +84,7 @@ module.exports = {
|
||||
```
|
||||
|
||||
#### Running the test suite
|
||||
|
||||
Please [see this]](./docs/contributing.md#run-the-regression-test-suite).
|
||||
The test suite currently only consists of JSON-parsing unit tests. A full end-to-end sip test suite should be added.
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
6
app.js
6
app.js
@@ -11,10 +11,6 @@ assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONE
|
||||
|
||||
const Srf = require('drachtio-srf');
|
||||
const srf = new Srf();
|
||||
const tracer = require('./tracer')(process.env.JAMBONES_OTEL_SERVICE_NAME || 'jambonz-feature-server');
|
||||
const api = require('@opentelemetry/api');
|
||||
srf.locals = {...srf.locals, otel: {tracer, api}};
|
||||
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
const opts = {
|
||||
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
|
||||
@@ -27,7 +23,6 @@ installSrfLocals(srf, logger);
|
||||
|
||||
const {
|
||||
initLocals,
|
||||
createRootSpan,
|
||||
getAccountDetails,
|
||||
normalizeNumbers,
|
||||
retrieveApplication,
|
||||
@@ -67,7 +62,6 @@ if (process.env.NODE_ENV === 'test') {
|
||||
|
||||
srf.use('invite', [
|
||||
initLocals,
|
||||
createRootSpan,
|
||||
getAccountDetails,
|
||||
normalizeNumbers,
|
||||
retrieveApplication,
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# Contributors are welcome!
|
||||
|
||||
So, you want to hack on jambonz? Maybe add some features, maybe help fix some bugs? Awesome, welcome aboard!
|
||||
|
||||
This brief document should get you started. Here you will find instructions showing how to set up your laptop to run the regression test suite (which you should always run before committing any changes), as well as some basic info on the structure of the code.
|
||||
|
||||
## Getting oriented
|
||||
|
||||
First of all, you are in the right place to begin hacking on jambonz. The jambonz-feature-server app is kinda the center of the universe for jambonz. Most of the core logic in jambonz is implemented here: things like the [webhook verbs](../lib/tasks), [session management](../lib/session), and the [client-side webhook implementation](../lib/utils/http-requestor.js). A common thing you might want to do, for instance, is to add support for an all-new verb, and this code base is where would do that.
|
||||
|
||||
This jambonz-feature-server app works together quite closely with a [drachtio server](https://github.com/drachtio/drachtio-server) and a Freeswitch. In fact, these three components are bundled together into a single VM/instance (or a Deployment, in Kubernetes) that we more generally refer to as "Feature Server". The Feature Server is a horizontally-scalable unit that is deployed behind the public-facing SBC elements of a jambonz cluster (the SBC is itself a separately scalable unit). The drachtio-server handles the SIP signaling, the Freeswitch handles media operations and speech vendor integration, and the jambonz-feature-server app orchestrates all of it via the use of [drachtio-srf](https://github.com/drachtio/drachtio-srf) and [drachtio-fsmrf](https://github.com/drachtio/drachtio-fsmrf).
|
||||
|
||||
## How to do things
|
||||
|
||||
First of all, please join our [slack channel](https://joinslack.jambonz.org) in order to coordinate with us on the work, i.e. to notify us of what you are doing and make sure that no one else is already working on the same thing.
|
||||
|
||||
To prepare to make changes, please fork the repo to your own Github account, make changes, test them on your own running jambonz cluster, then run the regression test suite and lint check before giving us a PR.
|
||||
|
||||
### lint
|
||||
|
||||
We have some opinionated conventions that you must follow - see our [eslintrc.json](../.eslintrc.json) for details. Make sure your code passes by running:
|
||||
|
||||
```bash
|
||||
npm run jslint
|
||||
```
|
||||
|
||||
### test suite
|
||||
|
||||
#### Generate speech credentials and create run-tests.sh
|
||||
|
||||
The test suite also requires you to provide speech credentials for both GCP and AWS. You will want to create a new file named `run-tests.sh` in the project folder. Make the file executable and then copy in the text below, substituting your speech credentials where indicated:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
GCP_JSON_KEY='{"type":"service_account","project_id":"...etc"}' \
|
||||
AWS_ACCESS_KEY_ID='your-aws-access-key-id' \
|
||||
AWS_SECRET_ACCESS_KEY='your-aws-secret-access-key' \
|
||||
AWS_REGION='us-east-1' \
|
||||
JWT_SECRET='foobar' \
|
||||
npm test
|
||||
```
|
||||
>> Note: The project's .gitignore file prevents this file from being sent to Github, so you do not need to worry about exposing your credentials. Just make sure you name if run-tests.sh and create it in the project folder
|
||||
|
||||
The GCP credential is the JSON service key in stringified format.
|
||||
|
||||
#### Install Docker
|
||||
|
||||
The test suite ralso equires [Docker](https://www.docker.com/) and docker-compose to be installed on your laptop. Docker is used to set up a network with all of the elements required to test the jambonz-feature-server in a black-box type of fashion.
|
||||
|
||||
Once you have docker installed, you can optionally make sure everything Docker-wise is working properly by running this command from the project folder:
|
||||
|
||||
```bash
|
||||
docker-compose -f test/docker-compose-testbed.yaml up -d
|
||||
```
|
||||
|
||||
This may take several minutes to complete, mainly because the mysql schema needs to be installed and seeded, but if successful the output should look like this:
|
||||
|
||||
```bash
|
||||
$ docker-compose -f test/docker-compose-testbed.yaml up -d
|
||||
Creating network "test_fs" with driver "bridge"
|
||||
Creating test_webhook-transcribe_1 ... done
|
||||
Creating test_webhook-decline_1 ... done
|
||||
Creating test_mysql_1 ... done
|
||||
Creating test_docker-host_1 ... done
|
||||
Creating test_webhook-gather_1 ... done
|
||||
Creating test_webhook-say_1 ... done
|
||||
Creating test_freeswitch_1 ... done
|
||||
Creating test_influxdb_1 ... done
|
||||
Creating test_redis_1 ... done
|
||||
Creating test_drachtio_1 ... done
|
||||
```
|
||||
|
||||
At that point, you can run `docker ps` to see all of the containers running
|
||||
|
||||
```bash
|
||||
docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
abbc3594f390 drachtio/drachtio-server:latest "/entrypoint.sh drac…" About a minute ago Up About a minute 0.0.0.0:9060->9022/tcp test_drachtio_1
|
||||
1f384a274f87 redis:5-alpine "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 0.0.0.0:16379->6379/tcp test_redis_1
|
||||
78d0bb6ec9b1 influxdb:1.8 "/entrypoint.sh infl…" 2 minutes ago Up 2 minutes 0.0.0.0:8086->8086/tcp test_influxdb_1
|
||||
9616ff790709 jambonz/webhook-test-scaffold:latest "/entrypoint.sh" 2 minutes ago Up 2 minutes 0.0.0.0:3102->3000/tcp test_webhook-gather_1
|
||||
7323ab273ff4 drachtio/drachtio-freeswitch-mrf:v1.10.1-full "/entrypoint.sh free…" 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:8022->8021/tcp test_freeswitch_1
|
||||
e45e7d28dbc7 mysql:5.7 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes (healthy) 33060/tcp, 0.0.0.0:3360->3306/tcp test_mysql_1
|
||||
b626e5f3067e qoomon/docker-host "/entrypoint.sh" 2 minutes ago Up 2 minutes test_docker-host_1
|
||||
b0a94b5e8941 jambonz/webhook-test-scaffold:latest "/entrypoint.sh" 2 minutes ago Up 2 minutes 0.0.0.0:3101->3000/tcp test_webhook-say_1
|
||||
f80adda48eb5 jambonz/webhook-test-scaffold:latest "/entrypoint.sh" 2 minutes ago Up 2 minutes 0.0.0.0:3103->3000/tcp test_webhook-transcribe_1
|
||||
223db4a9c670 jambonz/webhook-test-scaffold:latest "/entrypoint.sh" 2 minutes ago Up 2 minutes 0.0.0.0:3100->3000/tcp test_webhook-decline_1
|
||||
```
|
||||
|
||||
#### Run the regression test suite
|
||||
|
||||
The test suite has a dependency that the mysql client is installed on your laptop/machine where the test will be run. This is needed in order to seed the mysql database that is running in the docker network.
|
||||
|
||||
Assuming you have installed the mysql client, and done the above steps, you should now be able to run the tests:
|
||||
|
||||
```bash
|
||||
./run-tests.sh
|
||||
```
|
||||
|
||||
If the docker network has not been started (as described above) it will start now, and this will take a minute or two. Otherwise, the test suite will start running immediately.
|
||||
|
||||
In evaluating the test results, be advised that the output is fairly verbose, and also in the process of shutting down once the tests are complete you will see a bunch of errors from redis (`@jambonz/realtimedb-helpers - redis error`). You can ignore these errors, they are just spit out by jambonz-feature-server as the test environment is torn down and it tries and fails to reconnect to redis.
|
||||
|
||||
The final output will indicate the number of tests run and passed:
|
||||
|
||||
```bash
|
||||
1..28
|
||||
# tests 28
|
||||
# pass 28
|
||||
|
||||
# ok
|
||||
```
|
||||
|
||||
#### Adding your own tests
|
||||
|
||||
Running a successful regression test means you haven't broken anything - Great!
|
||||
|
||||
It doesn't, of course, mean that your shiny new feature or bugfix works. Adding a new test case to the suite is (unfortunately) non-trivial. We will add more documentation in the future with a how-to guide on that, but be advised it does require knowledge of the SIP protocol and the [SIPp](http://sipp.sourceforge.net/doc/reference.html) tool.
|
||||
|
||||
For now, if you are unable to add tests to the regression suite, please do test your feature as thoroughly as you can on your own jambonz cluster before giving us a pull request.
|
||||
|
||||
|
||||
|
||||
@@ -8,18 +8,16 @@ const SipError = require('drachtio-srf').SipError;
|
||||
const sysError = require('./error');
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
const WsRequestor = require('../../utils/ws-requestor');
|
||||
const RootSpan = require('../../utils/call-tracer');
|
||||
const dbUtils = require('../../utils/db-utils');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const accountSid = req.body.account_sid;
|
||||
const {srf} = require('../../..');
|
||||
|
||||
logger.debug({body: req.body}, 'got createCall request');
|
||||
try {
|
||||
let uri, cs, to;
|
||||
const restDial = makeTask(logger, {'rest:dial': req.body});
|
||||
const {srf} = require('../../..');
|
||||
const {lookupAccountDetails} = dbUtils(logger, srf);
|
||||
const {getSBC, getFreeswitch} = srf.locals;
|
||||
const sbcAddress = getSBC();
|
||||
@@ -41,7 +39,7 @@ router.post('/', async(req, res) => {
|
||||
'X-Jambonz-Routing': target.type,
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
'X-Call-Sid': callSid,
|
||||
'X-Account-Sid': accountSid
|
||||
'X-Account-Sid': req.body.account_sid
|
||||
};
|
||||
|
||||
switch (target.type) {
|
||||
@@ -50,7 +48,7 @@ router.post('/', async(req, res) => {
|
||||
uri = `sip:${target.number}@${sbcAddress}`;
|
||||
to = target.number;
|
||||
if ('teams' === target.type) {
|
||||
const obj = await lookupTeamsByAccount(accountSid);
|
||||
const obj = await lookupTeamsByAccount(req.body.account_sid);
|
||||
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
|
||||
Object.assign(opts.headers, {
|
||||
'X-MS-Teams-FQDN': obj.ms_teams_fqdn,
|
||||
@@ -77,7 +75,7 @@ router.post('/', async(req, res) => {
|
||||
if (target.type === 'phone' && target.trunk) {
|
||||
const {lookupCarrier} = dbUtils(this.logger, srf);
|
||||
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
||||
logger.info(
|
||||
this.logger.info(
|
||||
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
||||
if (voip_carrier_sid) {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
@@ -118,39 +116,23 @@ router.post('/', async(req, res) => {
|
||||
* attach our requestor and notifier objects
|
||||
* these will be used for all http requests we make during this call
|
||||
*/
|
||||
if ('WS' === app.call_hook?.method || /^wss?:/.test(app.call_hook.url)) {
|
||||
logger.debug({call_hook: app.call_hook}, 'creating websocket for call hook');
|
||||
if ('WS' === app.call_hook?.method) {
|
||||
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
||||
if (app.call_hook.url === app.call_status_hook.url || !app.call_status_hook?.url) {
|
||||
logger.debug('reusing websocket for call status hook');
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
else {
|
||||
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook');
|
||||
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||
}
|
||||
if (!app.notifier && app.call_status_hook) {
|
||||
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
||||
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
|
||||
}
|
||||
else if (!app.notifier) {
|
||||
logger.debug('creating null call status hook');
|
||||
app.notifier = {request: () => {}};
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook,
|
||||
account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
}
|
||||
|
||||
/* now launch the outdial */
|
||||
try {
|
||||
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
||||
cbRequest: (err, inviteReq) => {
|
||||
/* in case of 302 redirect, this gets called twice, ignore the second
|
||||
except to update the req so that it can later be canceled if need be
|
||||
*/
|
||||
if (res.headersSent) {
|
||||
logger.info(`create-call: got redirect, updating request to new call-id ${req.get('Call-ID')}`);
|
||||
if (cs) cs.req = inviteReq;
|
||||
return;
|
||||
}
|
||||
/* in case of 302 redirect, this gets called twice, ignore the second */
|
||||
if (res.headersSent) return;
|
||||
|
||||
if (err) {
|
||||
logger.error(err, 'createCall Error creating call');
|
||||
@@ -158,22 +140,9 @@ router.post('/', async(req, res) => {
|
||||
return;
|
||||
}
|
||||
inviteReq.srf = srf;
|
||||
inviteReq.locals = {
|
||||
...(inviteReq || {}),
|
||||
callSid,
|
||||
application_sid: app.application_sid
|
||||
};
|
||||
/* ok our outbound INVITE is in flight */
|
||||
|
||||
const tasks = [restDial];
|
||||
const rootSpan = new RootSpan('rest-call', inviteReq);
|
||||
sipLogger = logger.child({
|
||||
callSid,
|
||||
callId: inviteReq.get('Call-ID'),
|
||||
accountSid,
|
||||
traceId: rootSpan.traceId
|
||||
});
|
||||
app.requestor.logger = app.notifier.logger = sipLogger;
|
||||
const callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
req: inviteReq,
|
||||
@@ -181,24 +150,17 @@ router.post('/', async(req, res) => {
|
||||
tag: app.tag,
|
||||
callSid,
|
||||
accountSid: req.body.account_sid,
|
||||
applicationSid: app.application_sid,
|
||||
traceId: rootSpan.traceId
|
||||
});
|
||||
cs = new RestCallSession({
|
||||
logger: sipLogger,
|
||||
application: app,
|
||||
srf,
|
||||
req: inviteReq,
|
||||
ep,
|
||||
tasks,
|
||||
callInfo,
|
||||
accountInfo,
|
||||
rootSpan
|
||||
applicationSid: app.application_sid
|
||||
});
|
||||
cs = new RestCallSession({logger, application: app, srf, req: inviteReq, ep, tasks, callInfo, accountInfo});
|
||||
cs.exec(req);
|
||||
|
||||
res.status(201).json({sid: cs.callSid});
|
||||
|
||||
sipLogger = logger.child({
|
||||
callSid: cs.callSid,
|
||||
callId: callInfo.callId
|
||||
});
|
||||
sipLogger.info(`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
|
||||
},
|
||||
cbProvisional: (prov) => {
|
||||
@@ -240,7 +202,6 @@ router.post('/', async(req, res) => {
|
||||
else console.error(err);
|
||||
}
|
||||
ep.destroy();
|
||||
setTimeout(restDial.kill.bind(restDial), 5000);
|
||||
}
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
|
||||
@@ -7,8 +7,6 @@ const makeTask = require('./tasks/make_task');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const normalizeJambones = require('./utils/normalize-jambones');
|
||||
const dbUtils = require('./utils/db-utils');
|
||||
const RootSpan = require('./utils/call-tracer');
|
||||
const listTaskNames = require('./utils/summarize-tasks');
|
||||
|
||||
module.exports = function(srf, logger) {
|
||||
const {
|
||||
@@ -19,18 +17,15 @@ module.exports = function(srf, logger) {
|
||||
lookupAppByTeamsTenant
|
||||
} = srf.locals.dbHelpers;
|
||||
const {lookupAccountDetails} = dbUtils(logger, srf);
|
||||
|
||||
function initLocals(req, res, next) {
|
||||
if (!req.has('X-Account-Sid')) {
|
||||
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
|
||||
return res.send(500);
|
||||
}
|
||||
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
|
||||
const account_sid = req.get('X-Account-Sid');
|
||||
req.locals = {callSid, account_sid};
|
||||
req.locals = {
|
||||
callSid,
|
||||
logger: logger.child({callId: req.get('Call-ID'), callSid})
|
||||
};
|
||||
if (req.has('X-Application-Sid')) {
|
||||
const application_sid = req.get('X-Application-Sid');
|
||||
logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
|
||||
req.locals.logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
|
||||
req.locals.application_sid = application_sid;
|
||||
}
|
||||
if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User');
|
||||
@@ -39,50 +34,19 @@ module.exports = function(srf, logger) {
|
||||
next();
|
||||
}
|
||||
|
||||
function createRootSpan(req, res, next) {
|
||||
const {callSid, account_sid} = req.locals;
|
||||
const rootSpan = new RootSpan('incoming-call', req);
|
||||
const traceId = rootSpan.traceId;
|
||||
|
||||
req.locals = {
|
||||
...req.locals,
|
||||
traceId,
|
||||
logger: logger.child({
|
||||
callId: req.get('Call-ID'),
|
||||
callSid,
|
||||
accountSid: account_sid,
|
||||
callingNumber: req.callingNumber,
|
||||
calledNumber: req.calledNumber,
|
||||
traceId}),
|
||||
rootSpan
|
||||
};
|
||||
|
||||
/**
|
||||
* end the span on final failure or cancel from caller;
|
||||
* otherwise it will be closed when sip dialog is destroyed
|
||||
*/
|
||||
req.once('cancel', () => {
|
||||
rootSpan.setAttributes({finalStatus: 487});
|
||||
rootSpan.end();
|
||||
});
|
||||
res.once('finish', () => {
|
||||
rootSpan.setAttributes({finalStatus: res.statusCode});
|
||||
res.statusCode >= 300 && rootSpan.end();
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve account information for the incoming call
|
||||
*/
|
||||
async function getAccountDetails(req, res, next) {
|
||||
const {rootSpan, account_sid} = req.locals;
|
||||
|
||||
const {span} = rootSpan.startChildSpan('lookupAccountDetails');
|
||||
if (!req.has('X-Account-Sid')) {
|
||||
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
|
||||
return res.send(500);
|
||||
}
|
||||
const account_sid = req.locals.account_sid = req.get('X-Account-Sid');
|
||||
|
||||
try {
|
||||
req.locals.accountInfo = await lookupAccountDetails(account_sid);
|
||||
span.end();
|
||||
if (!req.locals.accountInfo.account.is_active) {
|
||||
logger.info(`Account is inactive or suspended ${account_sid}`);
|
||||
// TODO: alert
|
||||
@@ -91,7 +55,6 @@ module.exports = function(srf, logger) {
|
||||
logger.debug({accountInfo: req.locals?.accountInfo?.account}, `retrieved account info for ${account_sid}`);
|
||||
next();
|
||||
} catch (err) {
|
||||
span.end();
|
||||
logger.info({err}, `Error retrieving account details for account ${account_sid}`);
|
||||
res.send(503, {headers: {'X-Reason': `No Account exists for sid ${account_sid}`}});
|
||||
}
|
||||
@@ -123,8 +86,7 @@ module.exports = function(srf, logger) {
|
||||
*/
|
||||
async function retrieveApplication(req, res, next) {
|
||||
const logger = req.locals.logger;
|
||||
const {accountInfo, account_sid, rootSpan} = req.locals;
|
||||
const {span} = rootSpan.startChildSpan('lookupApplication');
|
||||
const {accountInfo, account_sid} = req.locals;
|
||||
try {
|
||||
let app;
|
||||
if (req.locals.application_sid) app = await lookupAppBySid(req.locals.application_sid);
|
||||
@@ -168,11 +130,6 @@ module.exports = function(srf, logger) {
|
||||
}
|
||||
}
|
||||
|
||||
span.setAttributes({
|
||||
'app.hook': app?.call_hook?.url,
|
||||
'application_sid': req.locals.application_sid
|
||||
});
|
||||
span.end();
|
||||
if (!app || !app.call_hook || !app.call_hook.url) {
|
||||
logger.info(`rejecting call to ${req.locals.calledNumber}: no application or webhook url`);
|
||||
return res.send(480, {
|
||||
@@ -206,15 +163,9 @@ module.exports = function(srf, logger) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {call_hook, call_status_hook, ...appInfo} = obj; // mask sensitive data like user/pass on webhook
|
||||
logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
||||
req.locals.callInfo = new CallInfo({
|
||||
req,
|
||||
app,
|
||||
direction: CallDirection.Inbound,
|
||||
traceId: rootSpan.traceId
|
||||
});
|
||||
req.locals.callInfo = new CallInfo({req, app, direction: CallDirection.Inbound});
|
||||
next();
|
||||
} catch (err) {
|
||||
span.end();
|
||||
logger.error(err, `${req.get('Call-ID')} Error looking up application for ${req.calledNumber}`);
|
||||
res.send(500);
|
||||
}
|
||||
@@ -225,9 +176,9 @@ module.exports = function(srf, logger) {
|
||||
*/
|
||||
async function invokeWebCallback(req, res, next) {
|
||||
const logger = req.locals.logger;
|
||||
const {rootSpan, application:app} = req.locals;
|
||||
let span;
|
||||
const app = req.locals.application;
|
||||
try {
|
||||
|
||||
if (app.tasks) {
|
||||
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
@@ -235,36 +186,12 @@ module.exports = function(srf, logger) {
|
||||
}
|
||||
/* retrieve the application to execute for this inbound call */
|
||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
|
||||
req.locals.callInfo, {
|
||||
defaults: {
|
||||
synthesizer: {
|
||||
vendor: app.speech_synthesis_vendor,
|
||||
language: app.speech_synthesis_language,
|
||||
voice: app.speech_synthesis_voice
|
||||
},
|
||||
recognizer: {
|
||||
vendor: app.speech_recognizer_vendor,
|
||||
language: app.speech_recognizer_language
|
||||
}
|
||||
}
|
||||
});
|
||||
logger.debug({params}, 'sending initial webhook');
|
||||
const obj = rootSpan.startChildSpan('performAppWebhook');
|
||||
span = obj.span;
|
||||
const b3 = rootSpan.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
||||
req.locals.callInfo);
|
||||
const json = await app.requestor.request('session:new', app.call_hook, params);
|
||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
span.setAttributes({
|
||||
'http.statusCode': 200,
|
||||
'app.tasks': listTaskNames(app.tasks)
|
||||
});
|
||||
span.end();
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
span?.setAttributes({webhookStatus: err.statusCode});
|
||||
span?.end();
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err?.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}});
|
||||
app.requestor.close();
|
||||
@@ -273,7 +200,6 @@ module.exports = function(srf, logger) {
|
||||
|
||||
return {
|
||||
initLocals,
|
||||
createRootSpan,
|
||||
getAccountDetails,
|
||||
normalizeNumbers,
|
||||
retrieveApplication,
|
||||
|
||||
@@ -8,15 +8,14 @@ const CallSession = require('./call-session');
|
||||
|
||||
*/
|
||||
class AdultingCallSession extends CallSession {
|
||||
constructor({logger, application, singleDialer, tasks, callInfo, accountInfo, rootSpan}) {
|
||||
constructor({logger, application, singleDialer, tasks, callInfo, accountInfo}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
srf: singleDialer.dlg.srf,
|
||||
tasks,
|
||||
callInfo,
|
||||
accountInfo,
|
||||
rootSpan
|
||||
accountInfo
|
||||
});
|
||||
this.sd = singleDialer;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ class CallInfo {
|
||||
let from ;
|
||||
let srf;
|
||||
this.direction = opts.direction;
|
||||
this.traceId = opts.traceId;
|
||||
if (opts.req) {
|
||||
const u = opts.req.getParsedHeader('from');
|
||||
const uri = parseUri(u.uri);
|
||||
@@ -115,7 +114,6 @@ class CallInfo {
|
||||
callStatus: this.callStatus,
|
||||
callerId: this.callerId,
|
||||
accountSid: this.accountSid,
|
||||
traceId: this.traceId,
|
||||
applicationSid: this.applicationSid,
|
||||
fsSipAddress: this.localSipAddress
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ class CallSession extends Emitter {
|
||||
* @param {array} opts.tasks - tasks we are to execute
|
||||
* @param {callInfo} opts.callInfo - information about the call
|
||||
*/
|
||||
constructor({logger, application, srf, tasks, callInfo, accountInfo, rootSpan, memberId, confName, confUuid}) {
|
||||
constructor({logger, application, srf, tasks, callInfo, accountInfo, memberId, confName, confUuid}) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.application = application;
|
||||
@@ -50,9 +50,6 @@ class CallSession extends Emitter {
|
||||
this.stackIdx = 0;
|
||||
this.callGone = false;
|
||||
this.notifiedComplete = false;
|
||||
this.rootSpan = rootSpan;
|
||||
|
||||
assert(rootSpan);
|
||||
|
||||
this.tmpFiles = new Set();
|
||||
|
||||
@@ -68,7 +65,6 @@ class CallSession extends Emitter {
|
||||
this._pool = srf.locals.dbHelpers.pool;
|
||||
|
||||
this.requestor.on('command', this._onCommand.bind(this));
|
||||
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,40 +226,26 @@ class CallSession extends Emitter {
|
||||
return this.backgroundGatherTask;
|
||||
}
|
||||
|
||||
get b3() {
|
||||
return this.rootSpan?.getTracingPropagation();
|
||||
}
|
||||
|
||||
async enableBotMode(gather, autoEnable) {
|
||||
async enableBotMode(gather) {
|
||||
try {
|
||||
const t = normalizeJambones(this.logger, [gather]);
|
||||
this.backgroundGatherTask = makeTask(this.logger, t[0]);
|
||||
this.backgroundGatherTask
|
||||
.once('dtmf', this._clearTasks.bind(this))
|
||||
.once('vad', this._clearTasks.bind(this))
|
||||
.once('transcription', this._clearTasks.bind(this))
|
||||
.once('timeout', this._clearTasks.bind(this));
|
||||
.on('dtmf', this._clearTasks.bind(this))
|
||||
.on('transcription', this._clearTasks.bind(this))
|
||||
.on('timeout', this._clearTasks.bind(this));
|
||||
this.logger.info({gather}, 'CallSession:enableBotMode - starting background gather');
|
||||
const resources = await this._evaluatePreconditions(this.backgroundGatherTask);
|
||||
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`);
|
||||
this.backgroundGatherTask.span = span;
|
||||
this.backgroundGatherTask.ctx = ctx;
|
||||
this.backgroundGatherTask.exec(this, resources)
|
||||
.then(() => {
|
||||
this.logger.info('CallSession:enableBotMode: gather completed');
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.span.end();
|
||||
this.backgroundGatherTask = null;
|
||||
if (autoEnable && !this.callGone && !this._stopping) {
|
||||
this.logger.info('CallSession:enableBotMode: restarting background gather');
|
||||
setImmediate(() => this.enableBotMode(gather, true));
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.info({err}, 'CallSession:enableBotMode: gather threw error');
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.span.end();
|
||||
this.backgroundGatherTask = null;
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -274,7 +256,7 @@ class CallSession extends Emitter {
|
||||
if (this.backgroundGatherTask) {
|
||||
try {
|
||||
this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask.kill().catch((err) => {});
|
||||
this.backgroundGatherTask.kill();
|
||||
} catch (err) {}
|
||||
this.backgroundGatherTask = null;
|
||||
}
|
||||
@@ -331,7 +313,7 @@ class CallSession extends Emitter {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
accessKeyId: credential.access_key_id,
|
||||
secretAccessKey: credential.secret_access_key,
|
||||
region: credential.aws_region || process.env.AWS_REGION
|
||||
region: process.env.AWS_REGION || credential.aws_region
|
||||
};
|
||||
}
|
||||
else if ('microsoft' === vendor) {
|
||||
@@ -380,16 +362,11 @@ class CallSession extends Emitter {
|
||||
this.backgroundGatherTask.updateTimeout(timeout);
|
||||
}
|
||||
else {
|
||||
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
|
||||
task.span = span;
|
||||
task.ctx = ctx;
|
||||
await task.exec(this, resources);
|
||||
task.span.end();
|
||||
}
|
||||
this.currentTask = null;
|
||||
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
} catch (err) {
|
||||
task.span?.end();
|
||||
this.currentTask = null;
|
||||
if (err.message?.includes(BADPRECONDITIONS)) {
|
||||
this.logger.info(`CallSession:exec task #${stackNum}:${taskNum}: ${task.name}: ${err.message}`);
|
||||
@@ -401,19 +378,10 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
if (0 === this.tasks.length && this.hasStableDialog && this.requestor instanceof WsRequestor) {
|
||||
let span;
|
||||
try {
|
||||
const {span} = this.rootSpan.startChildSpan('waiting for commands');
|
||||
const {reason, queue, command} = await this._awaitCommandsOrHangup();
|
||||
span.setAttributes({
|
||||
'completion.reason': reason,
|
||||
'async.request.queue': queue,
|
||||
'async.request.command': command
|
||||
});
|
||||
span.end();
|
||||
await this._awaitCommandsOrHangup();
|
||||
if (!this.hasStableDialog || this.callGone) break;
|
||||
} catch (err) {
|
||||
span.end();
|
||||
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
||||
break;
|
||||
}
|
||||
@@ -422,8 +390,6 @@ class CallSession extends Emitter {
|
||||
|
||||
// all done - cleanup
|
||||
this.logger.info('CallSession:exec all tasks complete');
|
||||
this._stopping = true;
|
||||
this.disableBotMode();
|
||||
this._onTasksDone();
|
||||
this._clearResources();
|
||||
|
||||
@@ -472,15 +438,17 @@ class CallSession extends Emitter {
|
||||
* this is called to clean up when the call is released from one side or another
|
||||
*/
|
||||
_callReleased() {
|
||||
this.logger.debug('CallSession:_callReleased - caller hung up');
|
||||
this.callGone = true;
|
||||
if (this.currentTask) {
|
||||
this.currentTask.kill(this);
|
||||
this.currentTask = null;
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver({reason: 'session ended'});
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
this.requestor && this.requestor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -518,20 +486,17 @@ class CallSession extends Emitter {
|
||||
async _lccCallHook(opts) {
|
||||
const webhooks = [];
|
||||
let sd, tasks, childTasks;
|
||||
const b3 = this.b3;
|
||||
const httpHeaders = b3 && {b3};
|
||||
|
||||
if (opts.call_hook || opts.child_call_hook) {
|
||||
if (opts.call_hook) {
|
||||
webhooks.push(this.requestor.request('session:redirect', opts.call_hook, this.callInfo.toJSON(), httpHeaders));
|
||||
webhooks.push(this.requestor.request('session:redirect', opts.call_hook, this.callInfo.toJSON()));
|
||||
}
|
||||
if (opts.child_call_hook) {
|
||||
/* child call hook only allowed from a connected Dial state */
|
||||
const task = this.currentTask;
|
||||
sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
webhooks.push(this.requestor.request(
|
||||
'session:redirect', opts.child_call_hook, sd.callInfo.toJSON(), httpHeaders));
|
||||
webhooks.push(this.requestor.request('session:redirect', opts.child_call_hook, sd.callInfo.toJSON()));
|
||||
}
|
||||
}
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
@@ -619,7 +584,7 @@ class CallSession extends Emitter {
|
||||
task.doConferenceMuteNonModerators(this, opts);
|
||||
}
|
||||
|
||||
async _lccSipRequest(opts, callSid) {
|
||||
async _lccSipRequest(opts) {
|
||||
const {sip_request} = opts;
|
||||
const {method, content_type, content, headers = {}} = sip_request;
|
||||
if (!this.hasStableDialog) {
|
||||
@@ -627,14 +592,13 @@ class CallSession extends Emitter {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dlg = callSid === this.callSid ? this.dlg : this.currentTask.dlg;
|
||||
const res = await dlg.request({
|
||||
const res = await this.dlg.request({
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': content_type
|
||||
},
|
||||
body: content
|
||||
'Content-Type': content_type,
|
||||
'Content': content
|
||||
}
|
||||
});
|
||||
this.logger.debug({res}, `CallSession:_lccSipRequest got response to ${method}`);
|
||||
return res;
|
||||
@@ -650,8 +614,6 @@ class CallSession extends Emitter {
|
||||
async _lccWhisper(opts, callSid) {
|
||||
const {whisper} = opts;
|
||||
let tasks;
|
||||
const b3 = this.b3;
|
||||
const httpHeaders = b3 && {b3};
|
||||
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
const task = this.currentTask;
|
||||
@@ -662,7 +624,7 @@ class CallSession extends Emitter {
|
||||
// allow user to provide a url object, a url string, an array of tasks, or a single task
|
||||
if (typeof whisper === 'string' || (typeof whisper === 'object' && whisper.url)) {
|
||||
// retrieve a url
|
||||
const json = await this.requestor(opts.call_hook, this.callInfo.toJSON(), httpHeaders);
|
||||
const json = await this.requestor(opts.call_hook, this.callInfo.toJSON());
|
||||
tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
}
|
||||
else if (Array.isArray(whisper)) {
|
||||
@@ -721,7 +683,7 @@ class CallSession extends Emitter {
|
||||
await this._lccConfMuteStatus(opts);
|
||||
}
|
||||
else if (opts.sip_request) {
|
||||
const res = await this._lccSipRequest(opts, callSid);
|
||||
const res = await this._lccSipRequest(opts);
|
||||
return {status: res.status, reason: res.reason};
|
||||
}
|
||||
|
||||
@@ -763,52 +725,8 @@ class CallSession extends Emitter {
|
||||
this.taskIdx = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append tasks to the current execution stack UNLESS there is a gather in the stack.
|
||||
* in that case, insert the tasks before the gather AND if the tasks include
|
||||
* a gather then delete/remove the gather from the existing stack
|
||||
* @param {*} t array of tasks
|
||||
*/
|
||||
_injectTasks(newTasks) {
|
||||
const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather);
|
||||
const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather;
|
||||
|
||||
this.logger.debug({
|
||||
currentTaskList: listTaskNames(this.tasks),
|
||||
newContent: listTaskNames(newTasks),
|
||||
currentlyExecutingGather,
|
||||
gatherPos
|
||||
}, 'CallSession:_injectTasks - starting');
|
||||
|
||||
const killGather = () => {
|
||||
this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content');
|
||||
this.currentTask.kill(this);
|
||||
};
|
||||
|
||||
if (-1 === gatherPos) {
|
||||
/* no gather in the stack simply append tasks */
|
||||
this.tasks.push(...newTasks);
|
||||
this.logger.debug({
|
||||
updatedTaskList: listTaskNames(this.tasks)
|
||||
}, 'CallSession:_injectTasks - completed (simple append)');
|
||||
|
||||
/* we do need to kill the current gather if we are executing one */
|
||||
if (currentlyExecutingGather) killGather();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentlyExecutingGather) killGather();
|
||||
const newTasksHasGather = newTasks.find((t) => t.name === TaskName.Gather);
|
||||
this.tasks.splice(gatherPos, newTasksHasGather ? 1 : 0, ...newTasks);
|
||||
|
||||
this.logger.debug({
|
||||
updatedTaskList: listTaskNames(this.tasks)
|
||||
}, 'CallSession:_injectTasks - completed');
|
||||
}
|
||||
|
||||
_onCommand({msgid, command, call_sid, queueCommand, data}) {
|
||||
_onCommand({msgid, command, queueCommand, data}) {
|
||||
this.logger.info({msgid, command, queueCommand}, 'CallSession:_onCommand - received command');
|
||||
const resolution = {reason: 'received command', queue: queueCommand, command};
|
||||
switch (command) {
|
||||
case 'redirect':
|
||||
if (Array.isArray(data)) {
|
||||
@@ -818,17 +736,11 @@ class CallSession extends Emitter {
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list');
|
||||
this.replaceApplication(t);
|
||||
}
|
||||
else if (process.env.JAMBONES_INJECT_CONTENT) {
|
||||
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks (injecting content)');
|
||||
this._injectTasks(t);
|
||||
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||
}
|
||||
else {
|
||||
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
|
||||
this.tasks.push(...t);
|
||||
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||
this.logger.debug({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||
}
|
||||
resolution.command = listTaskNames(t);
|
||||
}
|
||||
else this._lccCallHook(data);
|
||||
break;
|
||||
@@ -838,7 +750,7 @@ class CallSession extends Emitter {
|
||||
break;
|
||||
|
||||
case 'mute:status':
|
||||
this._lccMuteStatus(call_sid, data);
|
||||
this._lccMuteStatus(this.callSid, data);
|
||||
break;
|
||||
|
||||
case 'conf:mute-status':
|
||||
@@ -854,11 +766,11 @@ class CallSession extends Emitter {
|
||||
break;
|
||||
|
||||
case 'whisper':
|
||||
this._lccWhisper(data, call_sid);
|
||||
this._lccWhisper(data, this.callSid);
|
||||
break;
|
||||
|
||||
case 'sip:request':
|
||||
this._lccSipRequest(data, call_sid)
|
||||
this._lccSipRequest(data)
|
||||
.catch((err) => {
|
||||
this.logger.info({err, data}, `CallSession:_onCommand - error sending ${data.method}`);
|
||||
});
|
||||
@@ -868,24 +780,10 @@ class CallSession extends Emitter {
|
||||
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..');
|
||||
this.wakeupResolver(resolution);
|
||||
this.logger.info('CallSession:_onCommand - got commands, waking up..');
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
else {
|
||||
const {span} = this.rootSpan.startChildSpan('async command');
|
||||
const {queue, command} = resolution;
|
||||
span.setAttributes({
|
||||
'async.request.queue': queue,
|
||||
'async.request.command': command
|
||||
});
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
_onWsConnectionDropped() {
|
||||
const {stats} = this.srf.locals;
|
||||
stats.increment('app.hook.remote_close');
|
||||
}
|
||||
|
||||
_evaluatePreconditions(task) {
|
||||
@@ -1029,8 +927,6 @@ class CallSession extends Emitter {
|
||||
}
|
||||
this.tmpFiles.clear();
|
||||
this.requestor && this.requestor.close();
|
||||
|
||||
this.rootSpan && this.rootSpan.end();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1063,13 +959,7 @@ class CallSession extends Emitter {
|
||||
async propagateAnswer() {
|
||||
if (!this.dlg) {
|
||||
assert(this.ep);
|
||||
this.dlg = await this.srf.createUAS(this.req, this.res, {
|
||||
headers: {
|
||||
'X-Trace-ID': this.req.locals.traceId,
|
||||
'X-Call-Sid': this.req.locals.callSid
|
||||
},
|
||||
localSdp: this.ep.local.sdp
|
||||
});
|
||||
this.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
|
||||
this.logger.debug('answered call');
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.wrapDialog(this.dlg);
|
||||
@@ -1257,10 +1147,9 @@ class CallSession extends Emitter {
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('CallSession: call terminated by jambones');
|
||||
this.rootSpan.setAttributes({'call.termination': 'hangup by jambonz'});
|
||||
origDestroy();
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver({reason: 'session ended'});
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
@@ -1320,15 +1209,9 @@ class CallSession extends Emitter {
|
||||
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
const {span} = this.rootSpan.startChildSpan(`call-status:${this.callInfo.callStatus}`);
|
||||
span.setAttributes(this.callInfo.toJSON());
|
||||
try {
|
||||
const b3 = this.b3;
|
||||
const httpHeaders = b3 && {b3};
|
||||
this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON(), httpHeaders);
|
||||
span.end();
|
||||
this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
span.end();
|
||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const CallSession = require('./call-session');
|
||||
|
||||
*/
|
||||
class ConfirmCallSession extends CallSession {
|
||||
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName, rootSpan}) {
|
||||
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
@@ -18,8 +18,7 @@ class ConfirmCallSession extends CallSession {
|
||||
callInfo,
|
||||
accountInfo,
|
||||
memberId,
|
||||
confName,
|
||||
rootSpan
|
||||
confName
|
||||
});
|
||||
this.dlg = dlg;
|
||||
this.ep = ep;
|
||||
|
||||
@@ -16,8 +16,7 @@ class InboundCallSession extends CallSession {
|
||||
application: req.locals.application,
|
||||
callInfo: req.locals.callInfo,
|
||||
accountInfo: req.locals.accountInfo,
|
||||
tasks: req.locals.application.tasks,
|
||||
rootSpan: req.locals.rootSpan
|
||||
tasks: req.locals.application.tasks
|
||||
});
|
||||
this.req = req;
|
||||
this.res = res;
|
||||
@@ -33,7 +32,6 @@ class InboundCallSession extends CallSession {
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
this.rootSpan.setAttributes({'call.termination': 'caller abandoned'});
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.NoAnswer,
|
||||
sipStatus: 487,
|
||||
@@ -45,7 +43,6 @@ class InboundCallSession extends CallSession {
|
||||
_onTasksDone() {
|
||||
if (!this.res.finalResponseSent) {
|
||||
if (this._mediaServerFailure) {
|
||||
this.rootSpan.setAttributes({'call.termination': 'media server failure'});
|
||||
this.logger.info('InboundCallSession:_onTasksDone generating 480 due to media server failure');
|
||||
this.res.send(480, {
|
||||
headers: {
|
||||
@@ -54,7 +51,6 @@ class InboundCallSession extends CallSession {
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.rootSpan.setAttributes({'call.termination': 'tasks completed without answering call'});
|
||||
this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite');
|
||||
this.res.send(603);
|
||||
}
|
||||
@@ -68,12 +64,11 @@ class InboundCallSession extends CallSession {
|
||||
_callerHungup() {
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.rootSpan.setAttributes({'call.termination': 'hangup by caller'});
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Completed,
|
||||
duration
|
||||
});
|
||||
this.logger.info('InboundCallSession: caller hung up');
|
||||
this.logger.debug('InboundCallSession: caller hung up');
|
||||
this._callReleased();
|
||||
this.req.removeAllListeners('cancel');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const moment = require('moment');
|
||||
* @extends CallSession
|
||||
*/
|
||||
class RestCallSession extends CallSession {
|
||||
constructor({logger, application, srf, req, ep, tasks, callInfo, accountInfo, rootSpan}) {
|
||||
constructor({logger, application, srf, req, ep, tasks, callInfo, accountInfo}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
@@ -16,8 +16,7 @@ class RestCallSession extends CallSession {
|
||||
callSid: callInfo.callSid,
|
||||
tasks,
|
||||
callInfo,
|
||||
accountInfo,
|
||||
rootSpan
|
||||
accountInfo
|
||||
});
|
||||
this.req = req;
|
||||
this.ep = ep;
|
||||
|
||||
@@ -529,9 +529,7 @@ class Conference extends Task {
|
||||
|
||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
|
||||
assert(!this._playSession);
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, cs.callInfo, httpHeaders);
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, cs.callInfo);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
@@ -551,8 +549,7 @@ class Conference extends Task {
|
||||
accountInfo: cs.accountInfo,
|
||||
memberId: this.memberId,
|
||||
confName: this.confName,
|
||||
tasks,
|
||||
rootSpan: cs.rootSpan
|
||||
tasks
|
||||
});
|
||||
await this._playSession.exec();
|
||||
this._playSession = null;
|
||||
@@ -585,14 +582,11 @@ class Conference extends Task {
|
||||
|
||||
_notifyConferenceEvent(cs, eventName, params = {}) {
|
||||
if (this.statusEvents.includes(eventName)) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
params.event = eventName;
|
||||
params.duration = (Date.now() - this.conferenceStartTime.getTime()) / 1000;
|
||||
if (!params.time) params.time = (new Date()).toISOString();
|
||||
if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount;
|
||||
cs.application.requestor
|
||||
.request('verb:hook', this.statusHook, Object.assign(params, this.statusParams, httpHeaders))
|
||||
cs.application.requestor.request('verb:hook', this.statusHook, Object.assign(params, this.statusParams))
|
||||
.catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,11 @@ class TaskConfig extends Task {
|
||||
'bargeIn'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if (this.bargeIn.enable) {
|
||||
if (this.hasBargeIn && this.bargeIn.enable === true) {
|
||||
this.gatherOpts = {
|
||||
verb: 'gather',
|
||||
timeout: 0,
|
||||
bargein: true,
|
||||
input: ['speech']
|
||||
bargein: true
|
||||
};
|
||||
[
|
||||
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
@@ -26,8 +25,7 @@ class TaskConfig extends Task {
|
||||
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
|
||||
});
|
||||
}
|
||||
if (this.bargeIn.sticky) this.autoEnable = true;
|
||||
this.preconditions = this.bargeIn.enable ? TaskPreconditions.Endpoint : TaskPreconditions.None;
|
||||
this.preconditions = this.hasBargeIn ? TaskPreconditions.Endpoint : TaskPreconditions.None;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Config; }
|
||||
@@ -36,21 +34,7 @@ class TaskConfig extends Task {
|
||||
|
||||
get hasRecognizer() { return Object.keys(this.recognizer).length; }
|
||||
|
||||
get summary() {
|
||||
const phrase = [];
|
||||
if (this.bargeIn.enable) phrase.push('enable barge-in');
|
||||
if (this.hasSynthesizer) {
|
||||
const {vendor:v, language:l, voice} = this.synthesizer;
|
||||
const s = `{${v},${l},${voice}}`;
|
||||
phrase.push(`set synthesizer${s}`);
|
||||
}
|
||||
if (this.hasRecognizer) {
|
||||
const {vendor:v, language:l} = this.recognizer;
|
||||
const s = `{${v},${l}}`;
|
||||
phrase.push(`set recognizer${s}`);
|
||||
}
|
||||
return `${this.name}{${phrase.join(',')}`;
|
||||
}
|
||||
get hasBargeIn() { return Object.keys(this.bargeIn).length; }
|
||||
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
@@ -76,19 +60,13 @@ class TaskConfig extends Task {
|
||||
: cs.speechRecognizerLanguage;
|
||||
this.logger.info({recognizer: this.recognizer}, 'Config: updated recognizer');
|
||||
}
|
||||
if ('enable' in this.bargeIn) {
|
||||
if (this.hasBargeIn) {
|
||||
if (this.gatherOpts) {
|
||||
this.gatherOpts.recognizer = this.hasRecognizer ?
|
||||
this.recognizer :
|
||||
{
|
||||
vendor: cs.speechRecognizerVendor,
|
||||
language: cs.speechRecognizerLanguage
|
||||
};
|
||||
this.logger.info({opts: this.gatherOpts}, 'Config: enabling bargeIn');
|
||||
cs.enableBotMode(this.gatherOpts, this.autoEnable);
|
||||
this.logger.debug({opts: this.gatherOpts}, 'Config: enabling bargeIn');
|
||||
cs.enableBotMode(this.gatherOpts);
|
||||
}
|
||||
else {
|
||||
this.logger.info('Config: disabling bargeIn');
|
||||
this.logger.debug('Config: disabling bargeIn');
|
||||
cs.disableBotMode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,20 +138,7 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
get summary() {
|
||||
if (this.target.length === 1) {
|
||||
const target = this.target[0];
|
||||
switch (target.type) {
|
||||
case 'phone':
|
||||
case 'teams':
|
||||
return `${this.name}{type=${target.type},number=${target.number}}`;
|
||||
case 'user':
|
||||
return `${this.name}{type=${target.type},name=${target.name}}`;
|
||||
case 'sip':
|
||||
return `${this.name}{type=${target.type},sipUri=${target.sipUri}}`;
|
||||
default:
|
||||
return `${this.name}`;
|
||||
}
|
||||
}
|
||||
if (this.target.length === 1) return `${this.name}{type=${this.target[0].type}}`;
|
||||
else return `${this.name}{${this.target.length} targets}`;
|
||||
}
|
||||
|
||||
@@ -226,11 +213,7 @@ class TaskDial extends Task {
|
||||
this.logger.debug('Dial:whisper executing tasks');
|
||||
while (tasks.length && !cs.callGone) {
|
||||
const task = tasks.shift();
|
||||
const {span, ctx} = this.startChildSpan(`whisper:${this.sayTask.summary}`);
|
||||
task.span = span;
|
||||
task.ctx = ctx;
|
||||
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
|
||||
span.end();
|
||||
}
|
||||
this.logger.debug('Dial:whisper tasks complete');
|
||||
if (!cs.callGone && this.epOther) {
|
||||
@@ -271,9 +254,6 @@ class TaskDial extends Task {
|
||||
const referring_call_sid = isChild ? callInfo.callSid : cs.callSid;
|
||||
const referred_call_sid = isChild ? callInfo.parentCallSid : this.sd.callSid;
|
||||
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
|
||||
const to = parseUri(req.getParsedHeader('Refer-To').uri);
|
||||
const by = parseUri(req.getParsedHeader('Referred-By').uri);
|
||||
this.logger.info({to}, 'refer to parsed');
|
||||
@@ -288,7 +268,7 @@ class TaskDial extends Task {
|
||||
referring_call_sid,
|
||||
referred_call_sid
|
||||
}
|
||||
}, httpHeaders);
|
||||
});
|
||||
res.send(202);
|
||||
this.logger.info('DialTask:handleRefer - sent 202 Accepted');
|
||||
} catch (err) {
|
||||
@@ -348,10 +328,8 @@ class TaskDial extends Task {
|
||||
const key = arr[1];
|
||||
const match = dtmfDetector.keyPress(key);
|
||||
if (match) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
this.logger.info({callSid}, `Dial:_onInfo triggered dtmf match: ${match}`);
|
||||
requestor.request('verb:hook', this.dtmfHook, {dtmf: match, ...callInfo.toJSON()}, httpHeaders)
|
||||
requestor.request('verb:hook', this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
@@ -406,11 +384,10 @@ class TaskDial extends Task {
|
||||
this._killOutdials();
|
||||
}, this.timeout * 1000);
|
||||
|
||||
this.span.setAttributes({'dial.target': JSON.stringify(this.target)});
|
||||
this.target.forEach(async(t) => {
|
||||
try {
|
||||
t.confirmHook = t.confirmHook || this.confirmHook;
|
||||
//t.method = t.method || this.confirmMethod || 'POST';
|
||||
t.url = t.url || this.confirmUrl;
|
||||
t.method = t.method || this.confirmMethod || 'POST';
|
||||
if (t.type === 'teams') t.teamsInfo = teamsInfo;
|
||||
if (t.type === 'user' && !t.name.includes('@') && !fqdn) {
|
||||
const user = t.name;
|
||||
@@ -443,9 +420,7 @@ class TaskDial extends Task {
|
||||
target: t,
|
||||
opts,
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo,
|
||||
rootSpan: cs.rootSpan,
|
||||
startSpan: this.startSpan.bind(this)
|
||||
accountInfo: cs.accountInfo
|
||||
});
|
||||
this.dials.set(sd.callSid, sd);
|
||||
|
||||
@@ -606,8 +581,8 @@ class TaskDial extends Task {
|
||||
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
||||
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.epOther, this.ep);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.epOther);
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.ep);
|
||||
|
||||
/* if we can release the media back to the SBC, do so now */
|
||||
if (this.canReleaseMedia) this._releaseMedia(cs, sd);
|
||||
|
||||
@@ -295,9 +295,9 @@ class Dialogflow extends Task {
|
||||
}
|
||||
|
||||
// if a final transcription, start a typing sound
|
||||
if (this.thinkingMusic && !transcription.isEmpty && transcription.isFinal &&
|
||||
if (this.thinkingSound > 0 && !transcription.isEmpty && transcription.isFinal &&
|
||||
transcription.confidence > 0.8) {
|
||||
ep.play(this.data.thinkingMusic).catch((err) => this.logger.info(err, 'Error playing typing sound'));
|
||||
ep.play(this.data.thinkingSound).catch((err) => this.logger.info(err, 'Error playing typing sound'));
|
||||
}
|
||||
|
||||
// interrupt playback on speaking if bargein = true
|
||||
@@ -405,8 +405,8 @@ class Dialogflow extends Task {
|
||||
this.dtmfEntry = dtmfEntry;
|
||||
this.digitBuffer = null;
|
||||
// if a final transcription, start a typing sound
|
||||
if (this.thinkingMusic) {
|
||||
ep.play(this.thinkingMusic).catch((err) => this.logger.info(err, 'Error playing typing sound'));
|
||||
if (this.thinkingSound > 0) {
|
||||
ep.play(this.thinkingSound).catch((err) => this.logger.info(err, 'Error playing typing sound'));
|
||||
}
|
||||
|
||||
// kill the current dialogflow, which will result in us getting an immediate intent
|
||||
@@ -453,10 +453,7 @@ class Dialogflow extends Task {
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results = {}) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const json = await this.cs.requestor.request('verb:hook', hook,
|
||||
{...results, ...cs.callInfo.toJSON()}, httpHeaders);
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, {...results, ...cs.callInfo.toJSON()});
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('../make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -302,8 +302,6 @@ class TaskEnqueue extends Task {
|
||||
|
||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) {
|
||||
const {lengthOfList, getListPosition} = cs.srf.locals.dbHelpers;
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
|
||||
assert(!this._playSession);
|
||||
if (this.killed) return [];
|
||||
@@ -319,7 +317,7 @@ class TaskEnqueue extends Task {
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
|
||||
}
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, params, httpHeaders);
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, params);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
@@ -349,8 +347,7 @@ class TaskEnqueue extends Task {
|
||||
ep: cs.ep,
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo,
|
||||
tasks: tasksToRun,
|
||||
rootSpan: cs.rootSpan
|
||||
tasks: tasksToRun
|
||||
});
|
||||
await this._playSession.exec();
|
||||
this._playSession = null;
|
||||
|
||||
@@ -36,17 +36,6 @@ class TaskGather extends Task {
|
||||
this.language = recognizer.language;
|
||||
this.hints = recognizer.hints || [];
|
||||
this.hintsBoost = recognizer.hintsBoost;
|
||||
this.profanityFilter = recognizer.profanityFilter;
|
||||
this.punctuation = !!recognizer.punctuation;
|
||||
this.enhancedModel = !!recognizer.enhancedModel;
|
||||
this.model = recognizer.model || 'command_and_search';
|
||||
this.words = !!recognizer.words;
|
||||
this.singleUtterance = recognizer.singleUtterance || true;
|
||||
this.diarization = !!recognizer.diarization;
|
||||
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
|
||||
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
|
||||
this.interactionType = recognizer.interactionType || 'unspecified';
|
||||
this.naicsCode = recognizer.naicsCode || 0;
|
||||
this.altLanguages = recognizer.altLanguages || [];
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
@@ -69,12 +58,8 @@ class TaskGather extends Task {
|
||||
this.digitBuffer = '';
|
||||
this._earlyMedia = this.data.earlyMedia === true;
|
||||
|
||||
if (this.say) {
|
||||
this.sayTask = makeTask(this.logger, {say: this.say}, this);
|
||||
}
|
||||
if (this.play) {
|
||||
this.playTask = makeTask(this.logger, {play: this.play}, this);
|
||||
}
|
||||
if (this.say) this.sayTask = makeTask(this.logger, {say: this.say}, this);
|
||||
if (this.play) this.playTask = makeTask(this.logger, {play: this.play}, this);
|
||||
if (!this.sayTask && !this.playTask) this.listenDuringPrompt = false;
|
||||
|
||||
this.parentTask = parentTask;
|
||||
@@ -139,26 +124,16 @@ class TaskGather extends Task {
|
||||
|
||||
try {
|
||||
if (this.sayTask) {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`);
|
||||
this.sayTask.span = span;
|
||||
this.sayTask.ctx = ctx;
|
||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.on('playDone', (err) => {
|
||||
span.end();
|
||||
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.debug('Gather: nested say task completed');
|
||||
if (!this.killed) startListening(cs, ep);
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.debug('Gather: say task completed');
|
||||
});
|
||||
}
|
||||
else if (this.playTask) {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
|
||||
this.playTask.span = span;
|
||||
this.playTask.ctx = ctx;
|
||||
this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.on('playDone', (err) => {
|
||||
span.end();
|
||||
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
this.logger.debug('Gather: nested play task completed');
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
if (!this.killed) startListening(cs, ep);
|
||||
});
|
||||
}
|
||||
@@ -181,12 +156,9 @@ class TaskGather extends Task {
|
||||
}
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.VadDetected);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.VadDetected);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.VadDetected);
|
||||
}
|
||||
|
||||
kill(cs) {
|
||||
@@ -194,13 +166,11 @@ class TaskGather extends Task {
|
||||
this._killAudio(cs);
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
this.playTask?.span.end();
|
||||
this.sayTask?.span.end();
|
||||
this._resolve('killed');
|
||||
}
|
||||
|
||||
updateTimeout(timeout) {
|
||||
this.logger.info(`TaskGather:updateTimeout - updating timeout to ${timeout}`);
|
||||
this.logger.info(`TaskGather:updateTimout - updating timeout to ${timeout}`);
|
||||
this.timeout = timeout;
|
||||
this._startTimer();
|
||||
}
|
||||
@@ -236,44 +206,30 @@ class TaskGather extends Task {
|
||||
if (this.vad?.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
else opts.RECOGNIZER_VAD_VOICE_MS = 125;
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
|
||||
if ('google' === this.vendor) {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
[
|
||||
['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;
|
||||
Object.assign(opts, {
|
||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
||||
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
|
||||
GOOGLE_SPEECH_MODEL: 'command_and_search'
|
||||
});
|
||||
if (this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
if (typeof this.hintsBoost === 'number') {
|
||||
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
|
||||
}
|
||||
}
|
||||
if (this.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
if ('unspecified' !== this.interactionType) {
|
||||
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
|
||||
if (this.altLanguages && this.altLanguages.length > 0) {
|
||||
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
opts.GOOGLE_SPEECH_MODEL = this.model;
|
||||
if (this.diarization && this.diarizationMinSpeakers > 0) {
|
||||
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
|
||||
if (this.profanityFilter === true) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
|
||||
}
|
||||
if (this.diarization && this.diarizationMaxSpeakers > 0) {
|
||||
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
|
||||
}
|
||||
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
else if (['aws', 'polly'].includes(this.vendor)) {
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
@@ -289,7 +245,6 @@ class TaskGather extends Task {
|
||||
});
|
||||
}
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
else if ('microsoft' === this.vendor) {
|
||||
if (this.sttCredentials) {
|
||||
@@ -301,19 +256,14 @@ class TaskGather extends Task {
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.altLanguages && this.altLanguages.length > 0) {
|
||||
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption && this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.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)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
@@ -343,15 +293,9 @@ class TaskGather extends Task {
|
||||
|
||||
_startTimer() {
|
||||
if (0 === this.timeout) return;
|
||||
if (this._timeoutTimer) {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._timeoutTimer = null;
|
||||
}
|
||||
assert(!this._timeoutTimer);
|
||||
this.logger.debug(`Gather:_startTimer: timeout ${this.timeout}`);
|
||||
this._timeoutTimer = setTimeout(() => {
|
||||
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
||||
}, this.timeout);
|
||||
this._timeoutTimer = setTimeout(() => this._resolve('timeout'), this.timeout);
|
||||
}
|
||||
|
||||
_clearTimer() {
|
||||
@@ -388,13 +332,9 @@ class TaskGather extends Task {
|
||||
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,
|
||||
@@ -414,13 +354,7 @@ class TaskGather extends Task {
|
||||
};
|
||||
}
|
||||
}
|
||||
if (evt.is_final) {
|
||||
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
|
||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
|
||||
return this._startTranscribing(ep);
|
||||
}
|
||||
this._resolve('speech', evt);
|
||||
}
|
||||
if (evt.is_final) this._resolve('speech', evt);
|
||||
else {
|
||||
/* google has a measure of stability:
|
||||
https://cloud.google.com/speech-to-text/docs/basics#streaming_responses
|
||||
@@ -432,20 +366,16 @@ class TaskGather extends Task {
|
||||
evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) {
|
||||
if (!this.playComplete) {
|
||||
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
|
||||
this.emit('vad');
|
||||
}
|
||||
this._killAudio(cs);
|
||||
}
|
||||
if (this.partialResultHook) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt},
|
||||
this.cs.callInfo, httpHeaders));
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
_onEndOfUtterance(cs, ep) {
|
||||
this.logger.debug('TaskGather:_onEndOfUtterance');
|
||||
this.logger.info('TaskGather:_onEndOfUtterance');
|
||||
if (this.bargein && this.minBargeinWordCount === 0) {
|
||||
this._killAudio(cs);
|
||||
}
|
||||
@@ -455,65 +385,43 @@ class TaskGather extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_onVadDetected(cs, ep) {
|
||||
if (this.bargein && this.minBargeinWordCount === 0) {
|
||||
this.logger.debug('TaskGather:_onVadDetected');
|
||||
this._killAudio(cs);
|
||||
this.emit('vad');
|
||||
}
|
||||
}
|
||||
|
||||
_onNoSpeechDetected(cs, ep) {
|
||||
if (!this.callSession.callGone && !this.killed) {
|
||||
this.logger.debug('TaskGather:_onNoSpeechDetected - listen again');
|
||||
return this._startTranscribing(ep);
|
||||
}
|
||||
this._resolve('timeout');
|
||||
}
|
||||
|
||||
async _resolve(reason, evt) {
|
||||
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
|
||||
if (this.resolved) return;
|
||||
|
||||
this.resolved = true;
|
||||
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
|
||||
clearTimeout(this.interDigitTimer);
|
||||
|
||||
this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.ep.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
|
||||
}
|
||||
|
||||
this._clearTimer();
|
||||
|
||||
if (this.callSession && this.callSession.callGone) {
|
||||
this.logger.debug('TaskGather:_resolve - call is gone, not invoking web callback');
|
||||
this.notifyTaskDone();
|
||||
return;
|
||||
if (reason.startsWith('dtmf')) {
|
||||
if (this.parentTask) this.parentTask.emit('dtmf', evt);
|
||||
else {
|
||||
this.emit('dtmf', evt);
|
||||
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (reason.startsWith('dtmf')) {
|
||||
if (this.parentTask) this.parentTask.emit('dtmf', evt);
|
||||
else {
|
||||
this.emit('dtmf', evt);
|
||||
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
}
|
||||
else if (reason.startsWith('speech')) {
|
||||
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
||||
else {
|
||||
this.emit('transcription', evt);
|
||||
await this.performAction({speech: evt, reason: 'speechDetected'});
|
||||
}
|
||||
else if (reason.startsWith('speech')) {
|
||||
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
||||
else {
|
||||
this.emit('transcription', evt);
|
||||
await this.performAction({speech: evt, reason: 'speechDetected'});
|
||||
}
|
||||
}
|
||||
else if (reason.startsWith('timeout')) {
|
||||
if (this.parentTask) this.parentTask.emit('timeout', evt);
|
||||
else {
|
||||
this.emit('timeout', evt);
|
||||
await this.performAction({reason: 'timeout'});
|
||||
}
|
||||
else if (reason.startsWith('timeout')) {
|
||||
if (this.parentTask) this.parentTask.emit('timeout', evt);
|
||||
else {
|
||||
this.emit('timeout', evt);
|
||||
await this.performAction({reason: 'timeout'});
|
||||
}
|
||||
}
|
||||
} catch (err) { /*already logged error*/ }
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,9 +289,7 @@ class Lex extends Task {
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, results, httpHeaders);
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -38,12 +38,7 @@ class TaskListen extends Task {
|
||||
if (this.playBeep) await this._playBeep(ep);
|
||||
if (this.transcribeTask) {
|
||||
this.logger.debug('TaskListen:exec - starting nested transcribe task');
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`);
|
||||
this.transcribeTask.span = span;
|
||||
this.transcribeTask.ctx = ctx;
|
||||
this.transcribeTask.exec(cs, ep)
|
||||
.then((result) => span.end())
|
||||
.catch((err) => span.end());
|
||||
this.transcribeTask.exec(cs, ep);
|
||||
}
|
||||
await this._startListening(cs, ep);
|
||||
await this.awaitTaskDone();
|
||||
|
||||
@@ -13,10 +13,6 @@ class TaskPlay extends Task {
|
||||
|
||||
get name() { return TaskName.Play; }
|
||||
|
||||
get summary() {
|
||||
return `${this.name}:{url=${this.url}}`;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
|
||||
@@ -31,15 +31,8 @@ class Rasa extends Task {
|
||||
|
||||
/* start the first gather */
|
||||
this.gatherTask = this._makeGatherTask(this.prompt);
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`);
|
||||
this.gatherTask.span = span;
|
||||
this.gatherTask.ctx = ctx;
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
.then(() => span.end())
|
||||
.catch((err) => {
|
||||
span.end();
|
||||
this.logger.info({err}, 'Rasa gather task returned error');
|
||||
});
|
||||
.catch((err) => this.logger.info({err}, 'Rasa gather task returned error'));
|
||||
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
@@ -125,15 +118,8 @@ class Rasa extends Task {
|
||||
if (botUtterance) {
|
||||
this.logger.debug({botUtterance}, 'Rasa:_onTranscription: got user utterance');
|
||||
this.gatherTask = this._makeGatherTask(botUtterance);
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.gatherTask.summary}`);
|
||||
this.gatherTask.span = span;
|
||||
this.gatherTask.ctx = ctx;
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
.then(() => span.end())
|
||||
.catch((err) => {
|
||||
span.end();
|
||||
this.logger.info({err}, 'Rasa gather task returned error');
|
||||
});
|
||||
.catch((err) => this.logger.info({err}, 'Rasa gather task returned error'));
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'botMessage', message: response})
|
||||
.then((redirected) => {
|
||||
|
||||
@@ -48,9 +48,7 @@ class TaskRestDial extends Task {
|
||||
cs.setDialog(dlg);
|
||||
|
||||
try {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const tasks = await cs.requestor.request('session:new', this.call_hook, cs.callInfo, httpHeaders);
|
||||
const tasks = await cs.requestor.request('verb:hook', this.call_hook, cs.callInfo);
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
||||
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
|
||||
@@ -19,7 +19,7 @@ class TaskSay extends Task {
|
||||
if (this.text[i].startsWith('silence_stream')) continue;
|
||||
return `${this.name}{text=${this.text[i].slice(0, 15)}${this.text[i].length > 15 ? '...' : ''}}`;
|
||||
}
|
||||
return `${this.name}{${this.text[0]}}`;
|
||||
return this.text[0];
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
@@ -51,64 +51,45 @@ class TaskSay extends Task {
|
||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
||||
vendor
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||
this.notifyError(`No speech credentials have been provisioned for ${vendor}`);
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
// synthesize all of the text elements
|
||||
let lastUpdated = false;
|
||||
|
||||
/* produce an audio segment from the provided text */
|
||||
const generateAudio = async(text) => {
|
||||
const filepath = (await Promise.all(this.text.map(async(text) => {
|
||||
if (this.killed) return;
|
||||
if (text.startsWith('silence_stream://')) return text;
|
||||
|
||||
/* otel: trace time for tts */
|
||||
const {span} = this.startChildSpan('tts-generation', {
|
||||
'tts.vendor': vendor,
|
||||
'tts.language': language,
|
||||
'tts.voice': voice
|
||||
});
|
||||
try {
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
text,
|
||||
vendor,
|
||||
language,
|
||||
voice,
|
||||
engine,
|
||||
salt,
|
||||
credentials
|
||||
});
|
||||
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
if (!servedFromCache && !lastUpdated) {
|
||||
lastUpdated = true;
|
||||
updateSpeechCredentialLastUsed(credentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
}
|
||||
span.setAttributes({'tts.cached': servedFromCache});
|
||||
span.end();
|
||||
return filePath;
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Error synthesizing tts');
|
||||
span.end();
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
text,
|
||||
vendor,
|
||||
language,
|
||||
voice,
|
||||
engine,
|
||||
salt,
|
||||
credentials
|
||||
}).catch((err) => {
|
||||
this.logger.info(err, 'Error synthesizing tts');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
||||
vendor,
|
||||
detail: err.message
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
this.notifyError(err.message || err);
|
||||
return;
|
||||
});
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
if (!servedFromCache && !lastUpdated) {
|
||||
lastUpdated = true;
|
||||
updateSpeechCredentialLastUsed(credentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
}
|
||||
};
|
||||
return filePath;
|
||||
}))).filter((fp) => fp && fp.length);
|
||||
|
||||
const arr = this.text.map((t) => generateAudio(t));
|
||||
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
||||
this.logger.debug({filepath}, 'synthesized files for tts');
|
||||
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
|
||||
let segment = 0;
|
||||
while (!this.killed && segment < filepath.length) {
|
||||
do {
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
||||
@@ -118,8 +99,7 @@ class TaskSay extends Task {
|
||||
await ep.play(filepath[segment]);
|
||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||
}
|
||||
segment++;
|
||||
}
|
||||
} while (!this.killed && ++segment < filepath.length);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'TaskSay:exec error');
|
||||
|
||||
@@ -26,12 +26,6 @@ class TaskSipRefer extends Task {
|
||||
try {
|
||||
this.notifyHandler = this._handleNotify.bind(this, cs, dlg);
|
||||
dlg.on('notify', this.notifyHandler);
|
||||
/* otel: trace time for tts */
|
||||
this.referSpan = this.startSpan('send-refer', {
|
||||
'refer.refer_to': referTo,
|
||||
'refer.referred_by': referredBy
|
||||
});
|
||||
|
||||
const response = await dlg.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
@@ -41,20 +35,16 @@ class TaskSipRefer extends Task {
|
||||
}
|
||||
});
|
||||
this.referStatus = response.status;
|
||||
this.referSpan.setAttributes({'refer.status_code': response.status});
|
||||
this.logger.info(`TaskSipRefer:exec - received ${this.referStatus} to REFER`);
|
||||
|
||||
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
|
||||
if (this.referStatus === 202) {
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
else {
|
||||
await this.performAction({refer_status: this.referStatus});
|
||||
}
|
||||
else await this.performAction({refer_status: this.referStatus});
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'TaskSipRefer:exec - error sending REFER');
|
||||
}
|
||||
this.referSpan?.end();
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
@@ -76,13 +66,9 @@ class TaskSipRefer extends Task {
|
||||
const status = arr[1];
|
||||
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
||||
if (this.eventHook) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
await cs.requestor.request('verb:hook', this.eventHook,
|
||||
{event: 'transfer-status', call_status: status}, httpHeaders);
|
||||
await cs.requestor.request('verb:hook', this.eventHook, {event: 'transfer-status', call_status: status});
|
||||
}
|
||||
if (status >= 200) {
|
||||
this.referSpan.setAttributes({'refer.finalNotify': status});
|
||||
await this.performAction({refer_status: 202, final_referred_call_status: status});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"bargeIn": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"sticky": "boolean",
|
||||
"actionHook": "object|string",
|
||||
"input": "array",
|
||||
"finishOnKey": "string",
|
||||
@@ -448,7 +447,6 @@
|
||||
"tag"
|
||||
]
|
||||
},
|
||||
"model": "string",
|
||||
"outputFormat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -4,7 +4,6 @@ const debug = require('debug')('jambonz:feature-server');
|
||||
const assert = require('assert');
|
||||
const {TaskPreconditions} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const {trace} = require('@opentelemetry/api');
|
||||
const specs = new Map();
|
||||
const _specData = require('./specs');
|
||||
for (const key in _specData) {specs.set(key, _specData[key]);}
|
||||
@@ -72,34 +71,6 @@ class Task extends Emitter {
|
||||
setImmediate(() => this.parentTask = null);
|
||||
}
|
||||
|
||||
startSpan(name, attributes) {
|
||||
const {srf} = require('../..');
|
||||
const {tracer} = srf.locals.otel;
|
||||
const span = tracer.startSpan(name, undefined, this.ctx);
|
||||
if (attributes) span.setAttributes(attributes);
|
||||
trace.setSpan(this.ctx, span);
|
||||
return span;
|
||||
}
|
||||
|
||||
startChildSpan(name, attributes) {
|
||||
const {srf} = require('../..');
|
||||
const {tracer} = srf.locals.otel;
|
||||
const span = tracer.startSpan(name, undefined, this.ctx);
|
||||
if (attributes) span.setAttributes(attributes);
|
||||
const ctx = trace.setSpan(this.ctx, span);
|
||||
return {span, ctx};
|
||||
}
|
||||
|
||||
getTracingPropagation(encoding, span) {
|
||||
// TODO: support encodings beyond b3 https://github.com/openzipkin/b3-propagation
|
||||
if (span) {
|
||||
return `${span.spanContext().traceId}-${span.spanContext().spanId}-1`;
|
||||
}
|
||||
if (this.span) {
|
||||
return `${this.span.spanContext().traceId}-${this.span.spanContext().spanId}-1`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* when a subclass Task has completed its work, it should call this method
|
||||
*/
|
||||
@@ -137,62 +108,32 @@ class Task extends Emitter {
|
||||
return this.callSession.normalizeUrl(url, method, auth);
|
||||
}
|
||||
|
||||
notifyError(errMsg) {
|
||||
const params = {error: errMsg, verb: this.name};
|
||||
this.cs.requestor.request('jambonz:error', '/error', params)
|
||||
.catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
|
||||
}
|
||||
|
||||
async performAction(results, expectResponse = true) {
|
||||
if (this.actionHook) {
|
||||
const params = results ? Object.assign(results, this.cs.callInfo.toJSON()) : this.cs.callInfo.toJSON();
|
||||
const span = this.startSpan('verb:hook', {'hook.url': this.actionHook});
|
||||
const b3 = this.getTracingPropagation('b3', span);
|
||||
const httpHeaders = b3 && {b3};
|
||||
span.setAttributes({'http.body': JSON.stringify(params)});
|
||||
try {
|
||||
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params, httpHeaders);
|
||||
span.setAttributes({'http.statusCode': 200});
|
||||
span.end();
|
||||
if (expectResponse && json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.callSession.replaceApplication(tasks);
|
||||
}
|
||||
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params);
|
||||
if (expectResponse && json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.callSession.replaceApplication(tasks);
|
||||
}
|
||||
} catch (err) {
|
||||
span.setAttributes({'http.statusCode': err.statusCode});
|
||||
span.end();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async performHook(cs, hook, results) {
|
||||
const span = this.startSpan('verb:hook', {'hook.url': hook});
|
||||
const b3 = this.getTracingPropagation('b3', span);
|
||||
const httpHeaders = b3 && {b3};
|
||||
span.setAttributes({'http.body': JSON.stringify(results)});
|
||||
try {
|
||||
const json = await cs.requestor.request('verb:hook', hook, results, httpHeaders);
|
||||
span.setAttributes({'http.statusCode': 200});
|
||||
span.end();
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.redirect(cs, tasks);
|
||||
return true;
|
||||
}
|
||||
const json = await cs.requestor.request('verb:hook', hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.redirect(cs, tasks);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
span.setAttributes({'http.statusCode': err.statusCode});
|
||||
span.end();
|
||||
throw err;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
redirect(cs, tasks) {
|
||||
|
||||
@@ -32,9 +32,7 @@ class TaskTranscribe extends Task {
|
||||
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;
|
||||
@@ -58,12 +56,11 @@ class TaskTranscribe extends Task {
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
|
||||
async exec(cs, ep, ep2) {
|
||||
async exec(cs, ep, parentTask) {
|
||||
super.exec(cs);
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
|
||||
this.ep = ep;
|
||||
this.ep2 = ep2;
|
||||
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
|
||||
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
@@ -79,9 +76,7 @@ class TaskTranscribe extends Task {
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
await this._startTranscribing(cs, ep, 1);
|
||||
if (this.separateRecognitionPerChannel && ep2) await this._startTranscribing(cs, ep2, 2);
|
||||
|
||||
await this._startTranscribing(cs, ep);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
|
||||
@@ -109,15 +104,11 @@ class TaskTranscribe extends Task {
|
||||
// hangup after 1 sec if we don't get a final transcription
|
||||
this._timer = setTimeout(() => this.notifyTaskDone(), 1000);
|
||||
}
|
||||
if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) {
|
||||
this.ep2.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||
}
|
||||
else this.notifyTaskDone();
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
|
||||
async _startTranscribing(cs, ep, channel) {
|
||||
async _startTranscribing(cs, ep) {
|
||||
const opts = {};
|
||||
|
||||
if (this.vad.enable) {
|
||||
@@ -126,28 +117,25 @@ class TaskTranscribe extends Task {
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
|
||||
if (this.vendor === 'google') {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
[
|
||||
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
|
||||
//['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
|
||||
['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;
|
||||
@@ -161,8 +149,15 @@ class TaskTranscribe extends Task {
|
||||
if (this.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
if ('unspecified' !== this.interactionType) {
|
||||
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
|
||||
|
||||
// additionally set model if appropriate
|
||||
if ('phone_call' === this.interactionType) opts.GOOGLE_SPEECH_MODEL = 'phone_call';
|
||||
else if (['voice_search', 'voice_command'].includes(this.interactionType)) {
|
||||
opts.GOOGLE_SPEECH_MODEL = 'command_and_search';
|
||||
}
|
||||
else opts.GOOGLE_SPEECH_MODEL = 'phone_call';
|
||||
}
|
||||
opts.GOOGLE_SPEECH_MODEL = this.model;
|
||||
else opts.GOOGLE_SPEECH_MODEL = 'phone_call';
|
||||
if (this.diarization && this.diarizationMinSpeakers > 0) {
|
||||
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
|
||||
}
|
||||
@@ -213,7 +208,6 @@ class TaskTranscribe extends Task {
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.altLanguages.length > 1) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
@@ -231,16 +225,15 @@ class TaskTranscribe extends Task {
|
||||
vendor: this.vendor,
|
||||
interim: this.interim ? true : false,
|
||||
locale: this.language,
|
||||
channels: /*this.separateRecognitionPerChannel ? 2 : */ 1
|
||||
channels: this.separateRecognitionPerChannel ? 2 : 1
|
||||
});
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, channel, evt) {
|
||||
this.logger.debug({evt, channel}, 'TaskTranscribe:_onTranscription');
|
||||
_onTranscription(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const language_code = evt.PrimaryLanguage?.Language || this.language;
|
||||
const alternatives = nbest ? nbest.map((n) => {
|
||||
return {
|
||||
confidence: n.Confidence,
|
||||
@@ -255,25 +248,13 @@ class TaskTranscribe extends Task {
|
||||
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
channel,
|
||||
language_code,
|
||||
alternatives
|
||||
};
|
||||
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) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
this.cs.requestor.request('verb:hook', this.transcriptionHook,
|
||||
Object.assign({speech: evt}, this.cs.callInfo), httpHeaders)
|
||||
this.cs.requestor.request('verb:hook', this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
|
||||
}
|
||||
if (this.parentTask) {
|
||||
@@ -286,13 +267,13 @@ class TaskTranscribe extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_onNoAudio(cs, ep, channel) {
|
||||
this.logger.debug(`TaskTranscribe:_onNoAudio restarting transcription on channel ${channel}`);
|
||||
_onNoAudio(cs, ep) {
|
||||
this.logger.debug('TaskTranscribe:_onNoAudio restarting transcription');
|
||||
this._transcribe(ep);
|
||||
}
|
||||
|
||||
_onMaxDurationExceeded(cs, ep, channel) {
|
||||
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded restarting transcription on channel ${channel}`);
|
||||
_onMaxDurationExceeded(cs, ep) {
|
||||
this.logger.debug('TaskTranscribe:_onMaxDurationExceeded restarting transcription');
|
||||
this._transcribe(ep);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
const {context, trace} = require('@opentelemetry/api');
|
||||
const {Dialog} = require('drachtio-srf');
|
||||
class RootSpan {
|
||||
constructor(callType, req) {
|
||||
let tracer, callSid, linkedSpanId;
|
||||
|
||||
if (req instanceof Dialog) {
|
||||
const dlg = req;
|
||||
tracer = dlg.srf.locals.otel.tracer;
|
||||
callSid = dlg.callSid;
|
||||
linkedSpanId = dlg.linkedSpanId;
|
||||
}
|
||||
else {
|
||||
tracer = req.srf.locals.otel.tracer;
|
||||
callSid = req.locals.callSid;
|
||||
}
|
||||
this._span = tracer.startSpan(callType || 'incoming-call');
|
||||
if (req instanceof Dialog) {
|
||||
const dlg = req;
|
||||
this._span.setAttributes({
|
||||
linkedSpanId,
|
||||
callId: dlg.sip.callId
|
||||
});
|
||||
}
|
||||
else {
|
||||
this._span.setAttributes({
|
||||
callSid,
|
||||
accountSid: req.get('X-Account-Sid'),
|
||||
applicationSid: req.locals.application_sid,
|
||||
callId: req.get('Call-ID'),
|
||||
externalCallId: req.get('X-CID')
|
||||
});
|
||||
}
|
||||
|
||||
this._ctx = trace.setSpan(context.active(), this._span);
|
||||
this.tracer = tracer;
|
||||
}
|
||||
|
||||
get context() {
|
||||
return this._ctx;
|
||||
}
|
||||
|
||||
get traceId() {
|
||||
return this._span.spanContext().traceId;
|
||||
}
|
||||
|
||||
get spanId() {
|
||||
return this._span.spanContext().spanId;
|
||||
}
|
||||
|
||||
get traceFlags() {
|
||||
return this._span.spanContext().traceFlags;
|
||||
}
|
||||
|
||||
getTracingPropagation(encoding) {
|
||||
// TODO: support encodings beyond b3 https://github.com/openzipkin/b3-propagation
|
||||
if (this._span && this.traceId !== '00000000000000000000000000000000') {
|
||||
return `${this.traceId}-${this.spanId}-1`;
|
||||
}
|
||||
}
|
||||
|
||||
setAttributes(attrs) {
|
||||
this._span.setAttributes(attrs);
|
||||
}
|
||||
|
||||
end() {
|
||||
this._span.end();
|
||||
}
|
||||
|
||||
startChildSpan(name, attributes) {
|
||||
const span = this.tracer.startSpan(name, attributes, this._ctx);
|
||||
const ctx = trace.setSpan(context.active(), span);
|
||||
return {span, ctx};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RootSpan;
|
||||
|
||||
@@ -59,22 +59,19 @@
|
||||
"Transcription": "google_transcribe::transcription",
|
||||
"EndOfUtterance": "google_transcribe::end_of_utterance",
|
||||
"NoAudioDetected": "google_transcribe::no_audio_detected",
|
||||
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded",
|
||||
"VadDetected": "google_transcribe::vad_detected"
|
||||
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded"
|
||||
},
|
||||
"AwsTranscriptionEvents": {
|
||||
"Transcription": "aws_transcribe::transcription",
|
||||
"EndOfTranscript": "aws_transcribe::end_of_transcript",
|
||||
"NoAudioDetected": "aws_transcribe::no_audio_detected",
|
||||
"MaxDurationExceeded": "aws_transcribe::max_duration_exceeded",
|
||||
"VadDetected": "aws_transcribe::vad_detected"
|
||||
"MaxDurationExceeded": "aws_transcribe::max_duration_exceeded"
|
||||
},
|
||||
"AzureTranscriptionEvents": {
|
||||
"Transcription": "azure_transcribe::transcription",
|
||||
"StartOfUtterance": "azure_transcribe::start_of_utterance",
|
||||
"EndOfUtterance": "azure_transcribe::end_of_utterance",
|
||||
"NoSpeechDetected": "azure_transcribe::no_speech_detected",
|
||||
"VadDetected": "azure_transcribe::vad_detected"
|
||||
"NoSpeechDetected": "azure_transcribe::no_speech_detected"
|
||||
},
|
||||
"ListenEvents": {
|
||||
"Connect": "mod_audio_fork::connect",
|
||||
@@ -115,7 +112,6 @@
|
||||
"session:redirect",
|
||||
"call:status",
|
||||
"queue:status",
|
||||
"dial:confirm",
|
||||
"verb:hook",
|
||||
"jambonz:error"
|
||||
],
|
||||
|
||||
@@ -48,7 +48,6 @@ module.exports = (logger, srf) => {
|
||||
const pp = pool.promise();
|
||||
|
||||
const lookupAccountDetails = async(account_sid) => {
|
||||
|
||||
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, account_sid);
|
||||
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
|
||||
const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
|
||||
|
||||
@@ -49,12 +49,8 @@ class HttpRequestor extends BaseRequestor {
|
||||
* @param {string} [hook.password] - if basic auth is protecting the endpoint
|
||||
* @param {object} [params] - request parameters
|
||||
*/
|
||||
async request(type, hook, params, httpHeaders = {}) {
|
||||
/* jambonz:error only sent over ws */
|
||||
if (type === 'jambonz:error') return;
|
||||
|
||||
async request(type, hook, params) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
const url = hook.url || hook;
|
||||
const method = hook.method || 'POST';
|
||||
@@ -68,8 +64,8 @@ class HttpRequestor extends BaseRequestor {
|
||||
let buf;
|
||||
try {
|
||||
const sigHeader = this._generateSigHeader(payload, this.secret);
|
||||
const headers = {...sigHeader, ...this.authHeader, ...httpHeaders};
|
||||
this.logger.debug({url, headers}, 'send webhook');
|
||||
const headers = {...sigHeader, ...this.authHeader};
|
||||
//this.logger.info({url, headers}, 'send webhook');
|
||||
buf = this._isRelativeUrl(url) ?
|
||||
await this.post(url, payload, headers) :
|
||||
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const Mrf = require('drachtio-fsmrf');
|
||||
const ip = require('ip');
|
||||
const localIp = ip.address();
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
const assert = require('assert');
|
||||
|
||||
@@ -31,7 +32,6 @@ function initMS(logger, wrapper, ms) {
|
||||
function installSrfLocals(srf, logger) {
|
||||
logger.debug('installing srf locals');
|
||||
assert(!srf.locals.dbHelpers);
|
||||
const {tracer} = srf.locals.otel;
|
||||
const {getSBC, lifecycleEmitter} = require('./sbc-pinger')(logger);
|
||||
const StatsCollector = require('@jambonz/stats-collector');
|
||||
const stats = srf.locals.stats = new StatsCollector(logger);
|
||||
@@ -49,11 +49,7 @@ function installSrfLocals(srf, logger) {
|
||||
assert.ok(arr, `Invalid syntax JAMBONES_FREESWITCH: ${process.env.JAMBONES_FREESWITCH}`);
|
||||
const opts = {address: arr[1], port: arr[2], secret: arr[3]};
|
||||
if (arr.length > 4) opts.advertisedAddress = arr[4];
|
||||
/* NB: originally for testing only, but for now all jambonz deployments
|
||||
have freeswitch installed locally alongside this app
|
||||
*/
|
||||
if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
|
||||
else if (process.env.JAMBONES_ESL_LISTEN_ADDRESS) opts.listenAddress = process.env.JAMBONES_ESL_LISTEN_ADDRESS;
|
||||
return opts;
|
||||
});
|
||||
logger.info({fsInventory}, 'freeswitch inventory');
|
||||
@@ -66,7 +62,7 @@ function installSrfLocals(srf, logger) {
|
||||
initMS(logger, val, ms);
|
||||
}
|
||||
catch (err) {
|
||||
logger.info({err}, `failed connecting to freeswitch at ${fs.address}, will retry shortly: ${err.message}`);
|
||||
logger.info(`failed connecting to freeswitch at ${fs.address}, will retry shortly`);
|
||||
}
|
||||
}
|
||||
// retry to connect to any that were initially offline
|
||||
@@ -78,7 +74,7 @@ function installSrfLocals(srf, logger) {
|
||||
const ms = await mrf.connect(val.opts);
|
||||
initMS(logger, val, ms);
|
||||
} catch (err) {
|
||||
logger.info({err}, `failed connecting to freeswitch at ${val.opts.address}, will retry shortly`);
|
||||
logger.info(`failed connecting to freeswitch at ${val.opts.address}, will retry shortly`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +127,7 @@ function installSrfLocals(srf, logger) {
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
}, logger, tracer);
|
||||
}, logger);
|
||||
const {
|
||||
client,
|
||||
updateCallStatus,
|
||||
@@ -156,7 +152,7 @@ function installSrfLocals(srf, logger) {
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger, tracer);
|
||||
}, logger);
|
||||
const {
|
||||
writeAlerts,
|
||||
AlertType
|
||||
@@ -166,13 +162,6 @@ function installSrfLocals(srf, logger) {
|
||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
||||
});
|
||||
|
||||
let localIp;
|
||||
try {
|
||||
localIp = ip.address();
|
||||
} catch (err) {
|
||||
logger.error({err}, 'installSrfLocals - error detecting local ipv4 address');
|
||||
}
|
||||
|
||||
srf.locals = {...srf.locals,
|
||||
dbHelpers: {
|
||||
client,
|
||||
@@ -207,6 +196,8 @@ function installSrfLocals(srf, logger) {
|
||||
getListPosition
|
||||
},
|
||||
parentLogger: logger,
|
||||
ipv4: localIp,
|
||||
serviceUrl: `http://${localIp}:${PORT}`,
|
||||
getSBC,
|
||||
getSmpp: () => {
|
||||
return process.env.SMPP_URL;
|
||||
@@ -217,11 +208,6 @@ function installSrfLocals(srf, logger) {
|
||||
writeAlerts,
|
||||
AlertType
|
||||
};
|
||||
|
||||
if (localIp) {
|
||||
srf.locals.ipv4 = localIp;
|
||||
srf.locals.serviceUrl = `http://${localIp}:${PORT}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = installSrfLocals;
|
||||
|
||||
@@ -4,18 +4,15 @@ const SipError = require('drachtio-srf').SipError;
|
||||
const {TaskPreconditions, CallDirection} = require('../utils/constants');
|
||||
const CallInfo = require('../session/call-info');
|
||||
const assert = require('assert');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const AdultingCallSession = require('../session/adulting-call-session');
|
||||
const deepcopy = require('deepcopy');
|
||||
const moment = require('moment');
|
||||
const stripCodecs = require('./strip-ancillary-codecs');
|
||||
const RootSpan = require('./call-tracer');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||
super();
|
||||
assert(target.type);
|
||||
|
||||
@@ -25,8 +22,6 @@ class SingleDialer extends Emitter {
|
||||
this.opts = opts;
|
||||
this.application = application;
|
||||
this.confirmHook = target.confirmHook;
|
||||
this.rootSpan = rootSpan;
|
||||
this.startSpan = startSpan;
|
||||
|
||||
this.bindings = logger.bindings();
|
||||
|
||||
@@ -76,7 +71,7 @@ class SingleDialer extends Emitter {
|
||||
};
|
||||
}
|
||||
this.ms = ms;
|
||||
let uri, to, inviteSpan;
|
||||
let uri, to;
|
||||
try {
|
||||
switch (this.target.type) {
|
||||
case 'phone':
|
||||
@@ -142,24 +137,13 @@ class SingleDialer extends Emitter {
|
||||
localSdp: this.ep.local.sdp
|
||||
});
|
||||
if (this.target.auth) opts.auth = this.target.auth;
|
||||
inviteSpan = this.startSpan('invite', {
|
||||
'invite.uri': uri,
|
||||
'invite.dest_type': this.target.type
|
||||
});
|
||||
|
||||
this.dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
||||
cbRequest: (err, req) => {
|
||||
if (err) {
|
||||
this.logger.error(err, 'SingleDialer:exec Error creating call');
|
||||
this.emit('callCreateFail', err);
|
||||
inviteSpan.setAttributes({
|
||||
'invite.status_code': 500,
|
||||
'invite.err': err.message
|
||||
});
|
||||
inviteSpan.end();
|
||||
return;
|
||||
}
|
||||
inviteSpan.setAttributes({'invite.call_id': req.get('Call-ID')});
|
||||
|
||||
/**
|
||||
* INVITE has been sent out
|
||||
@@ -172,8 +156,7 @@ class SingleDialer extends Emitter {
|
||||
parentCallInfo: this.parentCallInfo,
|
||||
req,
|
||||
to,
|
||||
callSid: this.callSid,
|
||||
traceId: this.rootSpan.traceId
|
||||
callSid: this.callSid
|
||||
});
|
||||
this.logger = srf.locals.parentLogger.child({
|
||||
callSid: this.callSid,
|
||||
@@ -210,9 +193,6 @@ class SingleDialer extends Emitter {
|
||||
});
|
||||
this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`);
|
||||
const connectTime = this.dlg.connectTime = moment();
|
||||
inviteSpan.setAttributes({'invite.status_code': 200});
|
||||
inviteSpan.end();
|
||||
|
||||
|
||||
/* race condition: we were killed just as call was answered */
|
||||
if (this.killed) {
|
||||
@@ -266,17 +246,10 @@ class SingleDialer extends Emitter {
|
||||
if (err.status === 487) status.callStatus = CallStatus.NoAnswer;
|
||||
else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy;
|
||||
this.logger.info(`SingleDialer:exec outdial failure ${err.status}`);
|
||||
inviteSpan.setAttributes({'invite.status_code': err.status});
|
||||
inviteSpan.end();
|
||||
}
|
||||
else {
|
||||
this.logger.error(err, 'SingleDialer:exec');
|
||||
status.sipStatus = 500;
|
||||
inviteSpan.setAttributes({
|
||||
'invite.status_code': 500,
|
||||
'invite.err': err.message
|
||||
});
|
||||
inviteSpan.end();
|
||||
}
|
||||
this.emit('callStatusChange', status);
|
||||
if (this.ep) this.ep.destroy();
|
||||
@@ -311,8 +284,8 @@ class SingleDialer extends Emitter {
|
||||
async _executeApp(confirmHook) {
|
||||
try {
|
||||
// retrieve set of tasks
|
||||
const json = await this.requestor.request('dial:confirm', confirmHook, this.callInfo.toJSON());
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
const tasks = await this.requestor.request(confirmHook, this.callInfo.toJSON());
|
||||
|
||||
// verify it contains only allowed verbs
|
||||
const allowedTasks = tasks.filter((task) => {
|
||||
return [
|
||||
@@ -332,9 +305,7 @@ class SingleDialer extends Emitter {
|
||||
dlg: this.dlg,
|
||||
ep: this.ep,
|
||||
callInfo: this.callInfo,
|
||||
accountInfo: this.accountInfo,
|
||||
tasks,
|
||||
rootSpan: this.rootSpan
|
||||
tasks
|
||||
});
|
||||
await cs.exec();
|
||||
|
||||
@@ -348,6 +319,7 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
|
||||
async doAdulting({logger, tasks, application}) {
|
||||
this.logger = logger;
|
||||
this.adulting = true;
|
||||
this.emit('adulting');
|
||||
if (this.ep) {
|
||||
@@ -358,21 +330,15 @@ class SingleDialer extends Emitter {
|
||||
else {
|
||||
await this.reAnchorMedia();
|
||||
}
|
||||
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.dlg.linkedSpanId = this.rootSpan.traceId;
|
||||
const rootSpan = new RootSpan('outbound-call', this.dlg);
|
||||
const newLogger = logger.child({traceId: rootSpan.traceId});
|
||||
const cs = new AdultingCallSession({
|
||||
logger: newLogger,
|
||||
logger: this.logger,
|
||||
singleDialer: this,
|
||||
application,
|
||||
callInfo: this.callInfo,
|
||||
accountInfo: this.accountInfo,
|
||||
tasks,
|
||||
rootSpan
|
||||
tasks
|
||||
});
|
||||
cs.exec().catch((err) => newLogger.error({err}, 'doAdulting: error executing session'));
|
||||
cs.exec();
|
||||
return cs;
|
||||
}
|
||||
|
||||
@@ -421,13 +387,9 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
function placeOutdial({
|
||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan
|
||||
}) {
|
||||
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||
const myOpts = deepcopy(opts);
|
||||
const sd = new SingleDialer({
|
||||
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan
|
||||
});
|
||||
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo, accountInfo});
|
||||
sd.exec(srf, ms, myOpts);
|
||||
return sd;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,7 @@ class WsRequestor extends BaseRequestor {
|
||||
this.connections = 0;
|
||||
this.messagesInFlight = new Map();
|
||||
this.maliciousClient = false;
|
||||
this.closedGracefully = false;
|
||||
this.backoffMs = 500;
|
||||
this.connectInProgress = false;
|
||||
this.queuedMsg = [];
|
||||
this.id = short.generate();
|
||||
this.closedByUs = false;
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
|
||||
@@ -36,7 +32,7 @@ class WsRequestor extends BaseRequestor {
|
||||
* @param {string} [hook.password] - if basic auth is protecting the endpoint
|
||||
* @param {object} [params] - request parameters
|
||||
*/
|
||||
async request(type, hook, params, httpHeaders = {}) {
|
||||
async request(type, hook, params) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
const url = hook.url || hook;
|
||||
|
||||
@@ -44,10 +40,6 @@ class WsRequestor extends BaseRequestor {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
|
||||
return;
|
||||
}
|
||||
if (this.closedGracefully) {
|
||||
this.logger.debug(`WsRequestor:request - discarding ${type} because we closed the socket`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'session:new') this.call_sid = params.callSid;
|
||||
|
||||
@@ -55,19 +47,11 @@ class WsRequestor extends BaseRequestor {
|
||||
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
|
||||
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
|
||||
const requestor = new HttpRequestor(this.logger, this.account_sid, hook, this.secret);
|
||||
return requestor.request(type, hook, params, httpHeaders);
|
||||
return requestor.request(type, hook, params);
|
||||
}
|
||||
|
||||
/* connect if necessary */
|
||||
if (!this.ws) {
|
||||
if (this.connectInProgress) {
|
||||
this.logger.debug(
|
||||
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
|
||||
this.queuedMsg.push({type, hook, params, httpHeaders});
|
||||
return;
|
||||
}
|
||||
this.connectInProgress = true;
|
||||
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection`);
|
||||
if (this.connections >= MAX_RECONNECTS) {
|
||||
throw new Error(`max attempts connecting to ${this.url}`);
|
||||
}
|
||||
@@ -78,46 +62,30 @@ class WsRequestor extends BaseRequestor {
|
||||
this.stats.histogram('app.hook.connect_time', rtt, ['hook_type:app']);
|
||||
} catch (err) {
|
||||
this.logger.info({url, err}, 'WsRequestor:request - failed connecting');
|
||||
this.connectInProgress = false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
assert(this.ws);
|
||||
|
||||
/* prepare and send message */
|
||||
let payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
if (type === 'session:new') this._sessionData = payload;
|
||||
if (type === 'session:reconnect') payload = this._sessionData;
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||
|
||||
const msgid = short.generate();
|
||||
const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {};
|
||||
const obj = {
|
||||
type,
|
||||
msgid,
|
||||
call_sid: this.call_sid,
|
||||
hook: type === 'verb:hook' ? url : undefined,
|
||||
data: {...payload},
|
||||
...b3
|
||||
};
|
||||
|
||||
const sendQueuedMsgs = () => {
|
||||
if (this.queuedMsg.length > 0) {
|
||||
for (const {type, hook, params, httpHeaders} of this.queuedMsg) {
|
||||
this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`);
|
||||
setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
|
||||
}
|
||||
this.queuedMsg.length = 0;
|
||||
}
|
||||
data: {...payload}
|
||||
};
|
||||
|
||||
//this.logger.debug({obj}, `websocket: sending (${url})`);
|
||||
|
||||
/* simple notifications */
|
||||
if (['call:status', 'jambonz:error', 'session:reconnect'].includes(type)) {
|
||||
if (['call:status', 'jambonz:error'].includes(type)) {
|
||||
this.ws.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
sendQueuedMsgs();
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -134,7 +102,6 @@ class WsRequestor extends BaseRequestor {
|
||||
/* save the message info for reply */
|
||||
const startAt = process.hrtime();
|
||||
this.messagesInFlight.set(msgid, {
|
||||
timer,
|
||||
success: (response) => {
|
||||
clearTimeout(timer);
|
||||
const rtt = this._roundTrip(startAt);
|
||||
@@ -151,14 +118,13 @@ class WsRequestor extends BaseRequestor {
|
||||
/* send the message */
|
||||
this.ws.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
sendQueuedMsgs();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closedGracefully = true;
|
||||
this.logger.info('WsRequestor:close closing socket');
|
||||
this.closedByUs = true;
|
||||
try {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
@@ -166,8 +132,6 @@ class WsRequestor extends BaseRequestor {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -180,21 +144,19 @@ class WsRequestor extends BaseRequestor {
|
||||
_connect() {
|
||||
assert(!this.ws);
|
||||
return new Promise((resolve, reject) => {
|
||||
const handshakeTimeout = process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ?
|
||||
parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) :
|
||||
1500;
|
||||
let opts = {
|
||||
followRedirects: true,
|
||||
maxRedirects: 2,
|
||||
handshakeTimeout,
|
||||
handshakeTimeout: 1000,
|
||||
maxPayload: 8096,
|
||||
};
|
||||
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
|
||||
|
||||
this
|
||||
.once('ready', (ws) => {
|
||||
this.ws = ws;
|
||||
this.removeAllListeners('not-ready');
|
||||
if (this.connections > 1) this.request('session:reconnect', this.url);
|
||||
if (this.connections++ > 0) this.request('session:reconnect', this.url);
|
||||
resolve();
|
||||
})
|
||||
.once('not-ready', (err) => {
|
||||
@@ -207,7 +169,6 @@ class WsRequestor extends BaseRequestor {
|
||||
}
|
||||
|
||||
_setHandlers(ws) {
|
||||
this.logger.debug('WsRequestor:_setHandlers');
|
||||
ws
|
||||
.once('open', this._onOpen.bind(this, ws))
|
||||
.once('close', this._onClose.bind(this))
|
||||
@@ -224,23 +185,18 @@ class WsRequestor extends BaseRequestor {
|
||||
}
|
||||
|
||||
_onOpen(ws) {
|
||||
this.logger.info({url: this.url}, `WsRequestor(${this.id}) - successfully connected`);
|
||||
if (this.ws) this.logger.info({old_ws: this.ws._socket.address()}, 'WsRequestor:_onOpen');
|
||||
assert(!this.ws);
|
||||
this.ws = ws;
|
||||
this.connectInProgress = false;
|
||||
this.connections++;
|
||||
this.emit('ready', ws);
|
||||
this.logger.info({url: this.url}, 'WsRequestor - successfully connected');
|
||||
}
|
||||
|
||||
_onClose(code) {
|
||||
this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`);
|
||||
if (this.connections > 0 && code !== 1000) {
|
||||
_onClose() {
|
||||
if (this.connections > 0) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
||||
this.emit('socket-closed');
|
||||
}
|
||||
else if (code === 1000) this.closedGracefully = true;
|
||||
this.ws?.removeAllListeners();
|
||||
this.ws && this.ws.removeAllListeners();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
@@ -257,19 +213,8 @@ class WsRequestor extends BaseRequestor {
|
||||
|
||||
_onSocketClosed() {
|
||||
this.ws = null;
|
||||
this.emit('connection-dropped');
|
||||
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
|
||||
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
|
||||
setTimeout(() => {
|
||||
this.logger.debug(
|
||||
{haveWs: !!this.ws, connectInProgress: this.connectInProgress},
|
||||
'WsRequestor:_onSocketClosed time to reconnect');
|
||||
if (!this.ws && !this.connectInProgress) {
|
||||
this.connectInProgress = true;
|
||||
this._connect().catch((err) => this.connectInProgress = false);
|
||||
}
|
||||
}, this.backoffMs);
|
||||
this.backoffMs = this.backoffMs < 2000 ? this.backoffMs * 2 : (this.backoffMs + 2000);
|
||||
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedByUs) {
|
||||
setTimeout(this._connect.bind(this), 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +229,7 @@ class WsRequestor extends BaseRequestor {
|
||||
/* messages must be JSON format */
|
||||
try {
|
||||
const obj = JSON.parse(content);
|
||||
const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
|
||||
const {type, msgid, command, queueCommand = false, data} = obj;
|
||||
|
||||
this.logger.debug({obj}, 'WsRequestor:request websocket: received');
|
||||
assert.ok(type, 'type property not supplied');
|
||||
@@ -298,14 +243,14 @@ class WsRequestor extends BaseRequestor {
|
||||
case 'command':
|
||||
assert.ok(command, 'command property not supplied');
|
||||
assert.ok(data, 'data property not supplied');
|
||||
this._recvCommand(msgid, command, call_sid, queueCommand, data);
|
||||
this._recvCommand(msgid, command, queueCommand, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert.ok(false, `invalid type property: ${type}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err, content}, 'WsRequestor:_onMessage - invalid incoming message');
|
||||
this.logger.info({err}, 'WsRequestor:_onMessage - invalid incoming message');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,10 +266,10 @@ class WsRequestor extends BaseRequestor {
|
||||
success && success(data);
|
||||
}
|
||||
|
||||
_recvCommand(msgid, command, call_sid, queueCommand, data) {
|
||||
_recvCommand(msgid, command, queueCommand, data) {
|
||||
// TODO: validate command
|
||||
this.logger.info({msgid, command, call_sid, queueCommand, data}, 'received command');
|
||||
this.emit('command', {msgid, command, call_sid, queueCommand, data});
|
||||
this.logger.info({msgid, command, queueCommand, data}, 'received command');
|
||||
this.emit('command', {msgid, command, queueCommand, data});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7750
package-lock.json
generated
7750
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.5",
|
||||
"version": "v0.7.4",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -21,51 +21,45 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node app",
|
||||
"test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=info ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
|
||||
"test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.6.18",
|
||||
"@cognigy/socket-client": "^4.5.5",
|
||||
"@jambonz/db-helpers": "^0.6.16",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.29",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.26",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.9",
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.3.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.3.1",
|
||||
"@opentelemetry/instrumentation": "^0.27.0",
|
||||
"@opentelemetry/resources": "^1.3.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.3.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.3.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.3.1",
|
||||
"aws-sdk": "^2.1152.0",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"aws-sdk": "^2.1073.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.2",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.0",
|
||||
"drachtio-srf": "^4.5.0",
|
||||
"express": "^4.18.1",
|
||||
"helmet": "^5.1.0",
|
||||
"ip": "^1.1.8",
|
||||
"moment": "^2.29.3",
|
||||
"parse-url": "^5.0.8",
|
||||
"drachtio-fsmrf": "^2.0.13",
|
||||
"drachtio-srf": "^4.4.61",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.14.0",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"short-uuid": "^4.2.0",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
"ws": "^8.8.0",
|
||||
"ws": "^8.5.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clear-module": "^4.1.2",
|
||||
"eslint": "^7.32.0",
|
||||
"async": "^3.2.0",
|
||||
"clear-module": "^4.1.1",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.5.3"
|
||||
"tape": "^5.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const test = require('tape') ;
|
||||
const exec = require('child_process').exec ;
|
||||
const async = require('async');
|
||||
|
||||
test('starting docker network..takes a bit for mysql and freeswitch to come up..patience..', (t) => {
|
||||
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const test = require('tape');
|
||||
const { sippUac } = require('./sipp')('test_sbc-inbound');
|
||||
const test = require('blue-tape');
|
||||
const { output, sippUac } = require('./sipp')('test_sbc-inbound');
|
||||
const debug = require('debug')('drachtio:sbc-inbound');
|
||||
const clearModule = require('clear-module');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
|
||||
61
tracer.js
61
tracer.js
@@ -1,61 +0,0 @@
|
||||
const opentelemetry = require('@opentelemetry/api');
|
||||
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
|
||||
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
|
||||
const { Resource } = require('@opentelemetry/resources');
|
||||
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
||||
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
|
||||
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
|
||||
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
|
||||
const { OTLPTraceExporter } = require ('@opentelemetry/exporter-trace-otlp-http');
|
||||
//const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
||||
//const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
|
||||
//const { PinoInstrumentation } = require('@opentelemetry/instrumentation-pino');
|
||||
|
||||
module.exports = (serviceName) => {
|
||||
if (process.env.JAMBONES_OTEL_ENABLED) {
|
||||
const {version} = require('./package.json');
|
||||
const provider = new NodeTracerProvider({
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: version,
|
||||
}),
|
||||
});
|
||||
|
||||
let exporter;
|
||||
if (process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST) {
|
||||
exporter = new JaegerExporter();
|
||||
}
|
||||
else if (process.env.OTEL_EXPORTER_ZIPKIN_URL) {
|
||||
exporter = new ZipkinExporter({url:process.env.OTEL_EXPORTER_ZIPKIN_URL});
|
||||
}
|
||||
else {
|
||||
exporter = new OTLPTraceExporter({
|
||||
url: process.OTEL_EXPORTER_COLLECTOR_URL
|
||||
});
|
||||
}
|
||||
|
||||
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
|
||||
// The maximum queue size. After the size is reached spans are dropped.
|
||||
maxQueueSize: 100,
|
||||
// The maximum batch size of every export. It must be smaller or equal to maxQueueSize.
|
||||
maxExportBatchSize: 10,
|
||||
// The interval between two consecutive exports
|
||||
scheduledDelayMillis: 500,
|
||||
// How long the export can run before it is cancelled
|
||||
exportTimeoutMillis: 30000,
|
||||
}));
|
||||
|
||||
// Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings
|
||||
provider.register();
|
||||
registerInstrumentations({
|
||||
instrumentations: [
|
||||
//new HttpInstrumentation(),
|
||||
//new ExpressInstrumentation(),
|
||||
//new PinoInstrumentation()
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return opentelemetry.trace.getTracer(serviceName);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user