Compare commits

..

6 Commits

Author SHA1 Message Date
Dave Horton
aeb392fac6 bugfix: dial in nested transcribe set proper order of endpoints so channel_tag for caller is 1 and called is 2 2022-09-11 09:26:57 +02:00
Dave Horton
dd73ad9add bugfix: send callInfo data with transcribe transcriptionHook 2022-09-11 09:25:48 +02:00
Dave Horton
5f6a622387 new signature for WsRequestor 2022-09-08 11:51:07 +02:00
Dave Horton
99759459be rework min barge in to require >min in one utterance 2022-09-08 09:47:29 +02:00
Dave Horton
949604dd70 sync deps 2022-09-07 23:54:49 +02:00
Dave Horton
889257d7db write service_provider_sid with alerts 2022-09-07 23:51:40 +02:00
41 changed files with 2957 additions and 6049 deletions

View File

@@ -5,12 +5,12 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: lts/*
node-version: 14
- run: npm ci
- run: npm run jslint
- run: docker pull drachtio/sipp

View File

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

View File

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

47
app.js
View File

@@ -15,6 +15,7 @@ const tracer = require('./tracer')(process.env.JAMBONES_OTEL_SERVICE_NAME || 'ja
const api = require('@opentelemetry/api');
srf.locals = {...srf.locals, otel: {tracer, api}};
const PORT = process.env.HTTP_PORT || 3000;
const opts = {level: process.env.JAMBONES_LOGLEVEL || 'info'};
const pino = require('pino');
const logger = pino(opts, pino.destination({sync: false}));
@@ -32,6 +33,17 @@ const {
invokeWebCallback
} = require('./lib/middleware')(srf, logger);
// HTTP
const express = require('express');
const helmet = require('helmet');
const app = express();
Object.assign(app.locals, {
logger,
srf
});
const httpRoutes = require('./lib/http-routes');
const InboundCallSession = require('./lib/session/inbound-call-session');
const SipRecCallSession = require('./lib/session/siprec-call-session');
@@ -70,6 +82,20 @@ srf.invite(async(req, res) => {
session.exec();
});
// HTTP
app.use(helmet());
app.use(helmet.hidePoweredBy());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use('/', httpRoutes);
app.use((err, req, res, next) => {
logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
const httpServer = app.listen(PORT);
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
const sessionTracker = srf.locals.sessionTracker = require('./lib/session/session-tracker');
sessionTracker.on('idle', () => {
if (srf.locals.lifecycleEmitter.operationalState === LifeCycleEvents.ScaleIn) {
@@ -77,30 +103,19 @@ sessionTracker.on('idle', () => {
srf.locals.lifecycleEmitter.scaleIn();
}
});
const getCount = () => sessionTracker.count;
const healthCheck = require('@jambonz/http-health-check');
let httpServer;
const createHttpListener = require('./lib/utils/http-listener');
createHttpListener(logger, srf)
.then(({server, app}) => {
httpServer = server;
healthCheck({app, logger, path: '/', fn: getCount});
return {server, app};
})
.catch((err) => {
logger.error(err, 'Error creating http listener');
});
healthCheck({app, logger, path: '/', fn: getCount});
setInterval(() => {
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
}, 20000);
}, 5000);
const disconnect = () => {
return new Promise ((resolve) => {
httpServer?.on('close', resolve);
httpServer?.close();
httpServer.on('close', resolve);
httpServer.close();
srf.disconnect();
srf.locals.mediaservers.forEach((ms) => ms.disconnect());
});

View File

@@ -48,5 +48,5 @@
"ens posarem en contacto",
"ara no estem disponibles",
"no hi som"
]
],
}

View File

@@ -3,7 +3,7 @@ const makeTask = require('../../tasks/make_task');
const RestCallSession = require('../../session/rest-call-session');
const CallInfo = require('../../session/call-info');
const {CallDirection, CallStatus} = require('../../utils/constants');
const uuidv4 = require('uuid-random');
const { v4: uuidv4 } = require('uuid');
const SipError = require('drachtio-srf').SipError;
const sysError = require('./error');
const HttpRequestor = require('../../utils/http-requestor');
@@ -41,8 +41,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,
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost})
'X-Account-Sid': accountSid
};
switch (target.type) {
@@ -128,15 +127,27 @@ router.post('/', async(req, res) => {
}
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);
app.requestor = new HttpRequestor(
logger,
account.service_provider_sid,
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);
app.notifier = new HttpRequestor(
logger,
account.service_provider_sid,
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: () => {}, close: () => {}};
app.notifier = {request: () => {}};
}
/* now launch the outdial */

View File

@@ -26,7 +26,13 @@ router.post('/:partner', async(req, res) => {
app.notifier = app.requestor;
}
else {
app.requestor = new HttpRequestor(logger, account.account_sid, hook, account.webhook_secret);
app.requestor = new HttpRequestor(
logger,
account.service_provider_sid,
account.account_sid,
hook,
account.webhook_secret
);
app.notifier = {request: () => {}};
}
@@ -34,7 +40,6 @@ router.post('/:partner', async(req, res) => {
carrier: req.params.partner,
messageSid: app.messageSid,
accountSid: app.accountSid,
serviceProviderSid: account.service_provider_sid,
applicationSid: app.applicationSid,
from: req.body.from,
to: req.body.to,

View File

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

View File

@@ -1,4 +1,4 @@
const uuidv4 = require('uuid-random');
const { v4: uuidv4 } = require('uuid');
const {CallDirection, AllowedSipRecVerbs} = require('./utils/constants');
const {parseSiprecPayload} = require('./utils/siprec-utils');
const CallInfo = require('./session/call-info');
@@ -17,7 +17,8 @@ module.exports = function(srf, logger) {
lookupAppByRegex,
lookupAppBySid,
lookupAppByRealm,
lookupAppByTeamsTenant
lookupAppByTeamsTenant,
lookupAccountBySid
} = srf.locals.dbHelpers;
const {
writeAlerts,
@@ -25,7 +26,7 @@ module.exports = function(srf, logger) {
} = srf.locals;
const {lookupAccountDetails} = dbUtils(logger, srf);
function initLocals(req, res, next) {
const initLocals = async(req, res, next) => {
const callId = req.get('Call-ID');
logger.info({callId}, 'new incoming call');
if (!req.has('X-Account-Sid')) {
@@ -34,7 +35,10 @@ module.exports = function(srf, logger) {
}
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, callId};
console.log(`account_sid: ${account_sid}`);
const account = await lookupAccountBySid(account_sid);
console.log(`account: ${JSON.stringify(account)}`);
req.locals = {callSid, account_sid, service_provider_sid: account?.service_provider_sid, callId};
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}`);
@@ -44,7 +48,7 @@ module.exports = function(srf, logger) {
if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN');
next();
}
};
function createRootSpan(req, res, next) {
const {callId, callSid, account_sid} = req.locals;
@@ -118,7 +122,6 @@ module.exports = function(srf, logger) {
const {span} = rootSpan.startChildSpan('lookupAccountDetails');
try {
req.locals.accountInfo = await lookupAccountDetails(account_sid);
req.locals.service_provider_sid = req.locals.accountInfo?.account?.service_provider_sid;
span.end();
if (!req.locals.accountInfo.account.is_active) {
logger.info(`Account is inactive or suspended ${account_sid}`);
@@ -162,7 +165,7 @@ module.exports = function(srf, logger) {
* Given the dialed DID/phone number, retrieve the application to invoke
*/
async function retrieveApplication(req, res, next) {
const {logger, accountInfo, account_sid, rootSpan} = req.locals;
const {logger, accountInfo, service_provider_sid, account_sid, rootSpan} = req.locals;
const {span} = rootSpan.startChildSpan('lookupApplication');
try {
let app;
@@ -225,30 +228,48 @@ module.exports = function(srf, logger) {
* create a requestor that we will use for all http requests we make during the call.
* also create a notifier for call status events (if not needed, its a no-op).
*/
/* allow for caching data - when caching treat retrieved data as immutable */
const app2 = process.env.JAMBONES_MYSQL_REFRESH_TTL ? JSON.parse(JSON.stringify(app)) : app;
if ('WS' === app.call_hook?.method ||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
app2.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
app2.notifier = app.requestor;
app2.call_hook.method = 'WS';
app.requestor = new WsRequestor(
logger,
service_provider_sid,
account_sid,
app.call_hook,
accountInfo.account.webhook_secret
) ;
app.notifier = app.requestor;
app.call_hook.method = 'WS';
}
else {
app2.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
if (app.call_status_hook) app2.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
accountInfo.account.webhook_secret);
else app2.notifier = {request: () => {}};
app.requestor = new HttpRequestor(
logger,
service_provider_sid,
account_sid,
app.call_hook,
accountInfo.account.webhook_secret
);
if (app.call_status_hook) {
app.notifier = new HttpRequestor(
logger,
service_provider_sid,
account_sid,
app.call_status_hook,
accountInfo.account.webhook_secret
);
}
else app.notifier = {request: () => {}};
}
req.locals.application = app2;
req.locals.application = app;
const obj = Object.assign({}, app);
delete obj.requestor;
delete obj.notifier;
// eslint-disable-next-line no-unused-vars
const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook
const {call_hook, call_status_hook, ...appInfo} = obj; // mask sensitive data like user/pass on webhook
logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
req.locals.callInfo = new CallInfo({
req,
app: app2,
app,
direction: CallDirection.Inbound,
traceId: rootSpan.traceId
});
@@ -275,9 +296,7 @@ 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,
{service_provider_sid: req.locals.service_provider_sid},
{
req.locals.callInfo, {
defaults: {
synthesizer: {
vendor: app.speech_synthesis_vendor,
@@ -320,6 +339,7 @@ module.exports = function(srf, logger) {
span?.setAttributes({webhookStatus: err.statusCode});
span?.end();
writeAlerts({
service_provider_sid: req.locals.service_provider_sid,
account_sid: req.locals.account_sid,
target_sid: req.locals.callSid,
alert_type: AlertType.INVALID_APP_PAYLOAD,

View File

@@ -1,6 +1,6 @@
const {CallDirection, CallStatus} = require('../utils/constants');
const parseUri = require('drachtio-srf').parseUri;
const uuidv4 = require('uuid-random');
const { v4: uuidv4 } = require('uuid');
/**
* @classdesc Represents the common information for all calls
* that is provided in call status webhooks
@@ -11,7 +11,6 @@ class CallInfo {
let srf;
this.direction = opts.direction;
this.traceId = opts.traceId;
this.callTerminationBy = undefined;
if (opts.req) {
const u = opts.req.getParsedHeader('from');
const uri = parseUri(u.uri);
@@ -22,8 +21,9 @@ class CallInfo {
// inbound call
const {app, req} = opts;
srf = req.srf;
this.callSid = req.locals.callSid,
this.accountSid = app.account_sid,
this.callSid = req.locals.callSid;
this.serviceProviderSid = req.locals.service_provider_sid;
this.accountSid = app.account_sid;
this.applicationSid = app.application_sid;
this.from = from || req.callingNumber;
this.to = req.calledNumber;
@@ -40,6 +40,7 @@ class CallInfo {
srf = req.srf;
this.callSid = callSid || uuidv4();
this.parentCallSid = parentCallInfo.callSid;
this.serviceProviderSid = parentCallInfo.serviceProviderSid;
this.accountSid = parentCallInfo.accountSid;
this.applicationSid = parentCallInfo.applicationSid;
this.from = from || req.callingNumber;
@@ -52,18 +53,20 @@ class CallInfo {
}
else if (this.direction === CallDirection.None) {
// outbound SMS
const {messageSid, accountSid, applicationSid, res} = opts;
const {messageSid, serviceProviderSid, accountSid, applicationSid, res} = opts;
srf = res.srf;
this.messageSid = messageSid;
this.serviceProviderSid = serviceProviderSid;
this.accountSid = accountSid;
this.applicationSid = applicationSid;
this.res = res;
}
else {
// outbound call triggered by REST
const {req, callSid, accountSid, applicationSid, to, tag} = opts;
const {req, callSid, accountSid, serviceProviderSid, applicationSid, to, tag} = opts;
srf = req.srf;
this.callSid = callSid;
this.serviceProviderSid = serviceProviderSid;
this.accountSid = accountSid;
this.applicationSid = applicationSid;
this.callStatus = CallStatus.Trying,
@@ -120,7 +123,7 @@ class CallInfo {
applicationSid: this.applicationSid,
fsSipAddress: this.localSipAddress
};
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName', 'callTerminationBy'].forEach((prop) => {
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
if (this[prop]) obj[prop] = this[prop];
});
if (typeof this.duration === 'number') obj.duration = this.duration;

View File

@@ -83,10 +83,6 @@ class CallSession extends Emitter {
this.requestor.on('command', this._onCommand.bind(this));
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', (newRequestor) => {
this.logger.info(`handover to new base url ${newRequestor.url}`);
this.application.requestor = newRequestor;
});
}
/**
@@ -205,6 +201,12 @@ class CallSession extends Emitter {
return this.direction === CallDirection.Inbound && this.res.finalResponseSent;
}
/**
* returns the account sid
*/
get serviceProviderSid() {
return this.callInfo.serviceProviderSid;
}
/**
* returns the account sid
*/
@@ -290,18 +292,6 @@ class CallSession extends Emitter {
return this._globalAltLanguages;
}
set globalSttPunctuation(punctuate) {
this._globalSttPunctuation = punctuate;
}
get globalSttPunctuation() {
return this._globalSttPunctuation;
}
hasGlobalSttPunctuation() {
return this._globalSttPunctuation !== undefined;
}
async notifyRecordOptions(opts) {
const {action} = opts;
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
@@ -449,10 +439,6 @@ class CallSession extends Emitter {
async enableBotMode(gather, autoEnable) {
try {
if (this.backgroundGatherTask) {
this.logger.info('CallSession:enableBotMode - bot mode currently enabled, stop it');
this.disableBotMode();
}
const t = normalizeJambones(this.logger, [gather]);
this.backgroundGatherTask = makeTask(this.logger, t[0]);
this._bargeInEnabled = true;
@@ -540,6 +526,7 @@ class CallSession extends Emitter {
this.logger.info({err}, `malformed google service_key provisioned for account ${sid}`);
writeAlerts({
alert_type: AlertType.TTS_FAILURE,
service_provider_sid: this.serviceProviderSid,
account_sid: this.accountSid,
vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
@@ -557,11 +544,7 @@ class CallSession extends Emitter {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key,
region: credential.region,
use_custom_stt: credential.use_custom_stt,
custom_stt_endpoint: credential.custom_stt_endpoint,
use_custom_tts: credential.use_custom_tts,
custom_tts_endpoint: credential.custom_tts_endpoint
region: credential.region
};
}
else if ('wellsaid' === vendor) {
@@ -574,6 +557,7 @@ class CallSession extends Emitter {
else {
writeAlerts({
alert_type: AlertType.STT_NOT_PROVISIONED,
service_provider_sid: this.serviceProviderSid,
account_sid: this.accountSid,
vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
@@ -596,19 +580,13 @@ class CallSession extends Emitter {
this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`);
try {
const resources = await this._evaluatePreconditions(task);
let skip = false;
this.currentTask = task;
if (TaskName.Gather === task.name && this.isBotModeEnabled) {
if (this.backgroundGatherTask.updateTaskInProgress(task)) {
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
skip = true;
}
else {
this.logger.info('CallSession:exec disabling bot mode to start gather with new options');
this.disableBotMode();
}
const timeout = task.timeout;
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
this.backgroundGatherTask.updateTimeout(timeout);
}
if (!skip) {
else {
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
task.span = span;
task.ctx = ctx;
@@ -656,7 +634,6 @@ class CallSession extends Emitter {
this._onTasksDone();
this._clearResources();
if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid);
}
@@ -1177,12 +1154,7 @@ class CallSession extends Emitter {
// need to allocate an endpoint
try {
if (!this.ms) this.ms = this.getMS();
const ep = await this.ms.createEndpoint({
headers: {
'X-Jambones-Call-ID': this.callId,
},
remoteSdp: this.req.body
});
const ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
//ep.cs = this;
this.ep = ep;
ep.set({
@@ -1212,7 +1184,6 @@ class CallSession extends Emitter {
} catch (err) {
if (err === CALLER_CANCELLED_ERR_MSG) {
this.logger.error(err, 'caller canceled quickly before we could respond, ending call');
this.callInfo.callTerminationBy = 'caller';
this._notifyCallStatusChange({
callStatus: CallStatus.NoAnswer,
sipStatus: 487,
@@ -1290,7 +1261,6 @@ class CallSession extends Emitter {
}
this.tmpFiles.clear();
this.requestor && this.requestor.close();
this.notifier && this.notifier.close();
this.rootSpan && this.rootSpan.end();
}
@@ -1328,8 +1298,7 @@ class CallSession extends Emitter {
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,
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
'X-Call-Sid': this.req.locals.callSid
},
localSdp: this.ep.local.sdp
});
@@ -1421,8 +1390,13 @@ class CallSession extends Emitter {
}
else {
this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found');
this.queueEventHookRequestor = new HttpRequestor(this.logger, this.accountSid,
r[0], this.webhook_secret);
this.queueEventHookRequestor = new HttpRequestor(
this.logger,
this.serviceProviderSid,
this.accountSid,
r[0],
this.webhook_secret
);
this.queueEventHook = r[0];
}
} catch (err) {
@@ -1483,8 +1457,7 @@ class CallSession extends Emitter {
headers: {
'Refer-To': referTo,
'Referred-By': `sip:${this.srf.locals.localSipAddress}`,
'X-Retain-Call-Sid': this.callSid,
'X-Account-Sid': this.accountSid
'X-Retain-Call-Sid': this.callSid
}
});
if ([200, 202].includes(res.status)) {
@@ -1522,9 +1495,8 @@ class CallSession extends Emitter {
dlg.connected = false;
dlg.destroy = origDestroy;
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.callInfo.callTerminationBy = 'jambonz';
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('CallSession: call terminated by jambonz');
this.logger.debug('CallSession: call terminated by jambones');
this.rootSpan.setAttributes({'call.termination': 'hangup by jambonz'});
origDestroy().catch((err) => this.logger.info({err}, 'CallSession - error destroying dialog'));
if (this.wakeupResolver) {

View File

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

View File

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

View File

@@ -36,8 +36,7 @@ class SipRecCallSession extends InboundCallSession {
headers: {
'Content-Type': 'application/sdp',
'X-Trace-ID': this.req.locals.traceId,
'X-Call-Sid': this.req.locals.callSid,
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
'X-Call-Sid': this.req.locals.callSid
},
localSdp: combinedSdp
});

View File

@@ -26,7 +26,7 @@ class TaskConfig extends Task {
});
}
if (this.bargeIn.sticky) this.autoEnable = true;
this.preconditions = (this.bargeIn.enable || this.record?.action || this.data.amd) ?
this.preconditions = (this.bargeIn.enable || this.record?.action) ?
TaskPreconditions.Endpoint :
TaskPreconditions.None;
}
@@ -54,20 +54,13 @@ class TaskConfig extends Task {
return `${this.name}{${phrase.join(',')}`;
}
async exec(cs, {ep} = {}) {
async exec(cs) {
await super.exec(cs);
if (this.data.amd) {
this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs));
try {
this.ep = ep;
this.startAmd(cs, ep, this, this.data.amd);
} catch (err) {
this.logger.info({err}, 'Config:exec - Error calling startAmd');
}
}
if (this.hasSynthesizer) {
@@ -105,9 +98,6 @@ class TaskConfig extends Task {
this.logger.info({altLanguages: this.recognizer.altLanguages}, 'Config: updated altLanguages');
cs.altLanguages = this.recognizer.altLanguages;
}
if ('punctuation' in this.recognizer) {
cs.globalSttPunctuation = this.recognizer.punctuation;
}
this.logger.info({
recognizer: this.recognizer,
isContinuousAsr: cs.isContinuousAsr
@@ -140,17 +130,12 @@ class TaskConfig extends Task {
async kill(cs) {
super.kill(cs);
if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
}
_onAmdEvent(cs, evt) {
this.logger.info({evt}, 'Config:_onAmdEvent');
const {actionHook} = this.data.amd;
this.performHook(cs, actionHook, evt)
.catch((err) => {
this.logger.error({err}, 'Config:_onAmdEvent - error calling actionHook');
});
this.performHook(cs, actionHook, evt);
}
}

View File

@@ -630,7 +630,7 @@ 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, {ep2: this.epOther, ep:this.ep});
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep});
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther});
if (this.startAmd) {
try {
@@ -689,10 +689,7 @@ class TaskDial extends Task {
_onAmdEvent(cs, evt) {
this.logger.info({evt}, 'Dial:_onAmdEvent');
const {actionHook} = this.data.amd;
this.performHook(cs, actionHook, evt)
.catch((err) => {
this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook');
});
this.performHook(cs, actionHook, evt);
}
}

View File

@@ -82,8 +82,6 @@ class TaskGather extends Task {
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
this.azureSttEndpointId = recognizer.azureSttEndpointId;
this.azureAudioLogging = recognizer.audioLogging;
}
else {
this.hints = [];
@@ -143,22 +141,11 @@ class TaskGather extends Task {
this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
'Gather:exec - applying global sttHints');
}
if (process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH &&
!this.isContinuousAsr &&
this.hints.length > 0 && this.hints.length <= 10) {
this.earlyHintsMatch = true;
this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled');
}
if (cs.hasAltLanguages) {
this.altLanguages = this.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'Gather:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation) {
this.punctuation = cs.globalSttPunctuation;
}
if (!this.isContinuousAsr && cs.isContinuousAsr) {
this.isContinuousAsr = true;
this.asrTimeout = cs.asrTimeout * 1000;
@@ -176,6 +163,7 @@ class TaskGather extends Task {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor
@@ -186,15 +174,10 @@ class TaskGather extends Task {
const startListening = (cs, ep) => {
this._startTimer();
// dont start asr timer until we have a transcription
//if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
if (this.input.includes('speech') && !this.listenDuringPrompt) {
this._initSpeech(cs, ep)
.then(() => {
if (this.killed) {
this.logger.info('Gather:exec - task was quickly killed so do not transcribe');
return;
}
this._startTranscribing(ep);
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
})
@@ -227,13 +210,7 @@ class TaskGather extends Task {
if (!this.killed) startListening(cs, ep);
});
}
else {
if (this.killed) {
this.logger.info('Gather:exec - task was immediately killed so do not transcribe');
return;
}
startListening(cs, ep);
}
else startListening(cs, ep);
if (this.input.includes('speech') && this.listenDuringPrompt) {
await this._initSpeech(cs, ep);
@@ -270,15 +247,10 @@ class TaskGather extends Task {
this._resolve('killed');
}
updateTaskInProgress(opts) {
if (!this.needsStt && opts.input.includes('speech')) {
this.logger.info('TaskGather:updateTaskInProgress - adding speech to a background gather');
return false; // this needs be handled by killing the background gather and starting a new one
}
const {timeout} = opts;
updateTimeout(timeout) {
this.logger.info(`TaskGather:updateTimeout - updating timeout to ${timeout}`);
this.timeout = timeout;
this._startTimer();
return true;
}
_onDtmf(cs, ep, evt) {
@@ -327,7 +299,7 @@ class TaskGather extends Task {
}
if ('google' === this.vendor) {
this.bugname = 'google_transcribe';
this.bugname = 'google_trancribe';
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
@@ -339,7 +311,6 @@ class TaskGather extends Task {
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
else if (this[arr[0]] === false) opts[arr[1]] = false;
});
if (this.hints.length > 0) {
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
@@ -365,7 +336,7 @@ class TaskGather extends Task {
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
}
else if (['aws', 'polly'].includes(this.vendor)) {
this.bugname = 'aws_transcribe';
this.bugname = 'aws_trancribe';
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
if (this.vocabularyFilterName) {
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
@@ -382,20 +353,12 @@ class TaskGather extends Task {
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
}
else if ('microsoft' === this.vendor) {
this.bugname = 'azure_transcribe';
this.bugname = 'azure_trancribe';
if (this.sttCredentials) {
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': api_key,
'AZURE_REGION': region
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
'AZURE_REGION': this.sttCredentials.region
});
if (this.azureSttEndpointId) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
}
else if (use_custom_stt && custom_stt_endpoint) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
}
}
if (this.hints && this.hints.length > 0) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
@@ -406,7 +369,6 @@ class TaskGather extends Task {
else {
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
}
if (this.azureAudioLogging) opts.AZURE_AUDIO_LOGGING = 1;
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
if (this.profanityOption && this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
@@ -438,6 +400,7 @@ class TaskGather extends Task {
const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error');
writeAlerts({
service_provider_sid: this.cs.serviceProviderSid,
account_sid: this.cs.accountSid,
alert_type: AlertType.STT_FAILURE,
vendor: this.vendor,
@@ -450,7 +413,8 @@ class TaskGather extends Task {
if (0 === this.timeout) return;
this._clearTimer();
this._timeoutTimer = setTimeout(() => {
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
if (this.isContinuousAsr) this._startAsrTimer();
else this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
}, this.timeout);
}
@@ -549,21 +513,8 @@ class TaskGather extends Task {
}
}
if (this.earlyHintsMatch && evt.is_final === false) {
const transcript = evt.alternatives[0].transcript?.toLowerCase();
if (this.hints.find((h) => h.toLowerCase() === transcript)) {
this.logger.debug({evt}, 'Gather:_onTranscription: early hint match');
this._resolve('speech', evt);
return;
}
}
/* count words for bargein feature */
const words = evt.alternatives[0].transcript.split(' ').length;
const bufferedWords = this._bufferedTranscripts.reduce((count, e) => {
return count + e.alternatives[0].transcript.split(' ').length;
}, 0);
if (evt.is_final) {
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
if ('microsoft' === this.vendor && finished === 'true') {
@@ -594,14 +545,14 @@ class TaskGather extends Task {
return this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
}
this._startAsrTimer();
return this._startTranscribing(ep);
if ('microsoft' !== this.vendor) this._startTranscribing(ep);
return;
}
else {
if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) {
this.logger.debug({evt, words, bufferedWords},
'TaskGather:_onTranscription - final transcript but < min barge words');
this._bufferedTranscripts.push(evt);
this._startTranscribing(ep);
if (this.bargein && words < this.minBargeinWordCount) {
this.logger.debug({evt},
`TaskGather:_onTranscription - got ${words} but minBargein is ${this.minBargeinWordCount}; discarding`);
if ('microsoft' !== this.vendor) this._startTranscribing(ep);
return;
}
else {
@@ -615,9 +566,9 @@ class TaskGather extends Task {
others do not.
*/
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) {
if (this.bargein && words >= this.minBargeinWordCount) {
if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to minBargein words');
this.emit('vad');
}
this._killAudio(cs);
@@ -684,7 +635,7 @@ class TaskGather extends Task {
}
this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
if (this.needsStt && this.ep && this.ep.connected) {
if (this.ep && this.ep.connected) {
this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
}

View File

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

View File

@@ -7,8 +7,6 @@ class TaskPlay extends Task {
this.preconditions = TaskPreconditions.Endpoint;
this.url = this.data.url;
this.seekOffset = this.data.seekOffset || -1;
this.timeoutSecs = this.data.timeoutSecs || -1;
this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true;
}
@@ -33,13 +31,8 @@ class TaskPlay extends Task {
} else {
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
}
} else {
const file = (this.timeoutSecs >= 0 || this.seekOffset >= 0) ?
{file: this.url, seekOffset: this.seekOffset, timeoutSecs: this.timeoutSecs} : this.url;
const result = await ep.play(file);
await this.performAction(Object.assign(result, {reason: 'playCompleted'}),
!(this.parentTask || cs.isConfirmCallSession));
}
else await ep.play(this.url);
}
} catch (err) {
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);

View File

@@ -11,7 +11,6 @@ class TaskRestDial extends Task {
super(logger, opts);
this.from = this.data.from;
this.fromHost = this.data.fromHost;
this.to = this.data.to;
this.call_hook = this.data.call_hook;
this.timeout = this.data.timeout || 60;

View File

@@ -1,107 +1,12 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const breakLengthyTextIfNeeded = (logger, text) => {
const chunkSize = 1000;
if (text.length <= chunkSize) return [text];
const result = [];
const isSSML = text.startsWith('<speak>');
let startPos = 0;
let charPos = isSSML ? 7 : 0; // skip <speak>
let tag;
//logger.debug({isSSML}, `breakLengthyTextIfNeeded: handling text of length ${text.length}`);
while (startPos + charPos < text.length) {
if (isSSML && !tag && text[startPos + charPos] === '<') {
const tagStartPos = ++charPos;
while (startPos + charPos < text.length) {
if (text[startPos + charPos] === '>') {
if (text[startPos + charPos - 1] === '\\') tag = null;
else if (!tag) tag = text.substring(startPos + tagStartPos, startPos + charPos - 1);
break;
}
if (!tag) {
const c = text[startPos + charPos];
if (c === ' ') {
tag = text.substring(startPos + tagStartPos, startPos + charPos);
//logger.debug(`breakLengthyTextIfNeeded: enter tag ${tag} (space)`);
break;
}
}
charPos++;
}
if (tag) {
//search for end of tag
//logger.debug(`breakLengthyTextIfNeeded: searching forward for </${tag}>`);
const e1 = text.indexOf(`</${tag}>`, startPos + charPos);
const e2 = text.indexOf('/>', startPos + charPos);
const tagEndPos = e1 === -1 ? e2 : e2 === -1 ? e1 : Math.min(e1, e2);
if (tagEndPos === -1) {
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} not found, exiting`);
} else {
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} found at ${tagEndPos}`);
charPos = tagEndPos + 1;
}
tag = null;
}
continue;
}
if (charPos < chunkSize) {
charPos++;
continue;
}
// start looking for a good break point
let chunkIt = false;
const a = text[startPos + charPos];
const b = text[startPos + charPos + 1];
if (/[\.!\?]/.test(a) && /\s/.test(b)) {
//logger.debug('breakLengthyTextIfNeeded: breaking at sentence end');
chunkIt = true;
}
if (chunkIt) {
charPos++;
const chunk = text.substr(startPos, charPos);
if (isSSML) {
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}</speak>`);
}
else result.push(chunk);
charPos = 0;
startPos += chunk.length;
//logger.debug({chunk: result[result.length - 1]},
// `breakLengthyTextIfNeeded: chunked; new starting pos ${startPos}`);
}
else charPos++;
}
// final chunk
if (startPos < text.length) {
const chunk = text.substr(startPos);
if (isSSML) {
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}`);
}
else result.push(chunk);
//logger.debug({chunk: result[result.length - 1]},
// `breakLengthyTextIfNeeded: final chunk; starting pos ${startPos} length ${chunk.length}`);
}
return result;
};
class TaskSay extends Task {
constructor(logger, opts, parentTask) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
this.text = (Array.isArray(this.data.text) ? this.data.text : [this.data.text])
.map((t) => breakLengthyTextIfNeeded(this.logger, t))
.flat();
this.text = Array.isArray(this.data.text) ? this.data.text : [this.data.text];
this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
this.synthesizer = this.data.synthesizer || {};
@@ -142,6 +47,7 @@ class TaskSay extends Task {
try {
if (!credentials) {
writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor
@@ -164,10 +70,6 @@ class TaskSay extends Task {
'tts.voice': voice
});
try {
if (vendor === 'microsoft' && this.synthesizer.azureServiceEndpoint) {
credentials.use_custom_tts = true;
credentials.custom_tts_endpoint = this.synthesizer.azureServiceEndpoint;
}
const {filePath, servedFromCache} = await synthAudio(stats, {
text,
vendor,
@@ -191,6 +93,7 @@ class TaskSay extends Task {
this.logger.info({err}, 'Error synthesizing tts');
span.end();
writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor,

View File

@@ -36,7 +36,6 @@ class TaskSipRefer extends Task {
method: 'REFER',
headers: {
...this.headers,
...(this.referToIsUri && {'X-Refer-To-Leave-Untouched': true}),
'Refer-To': referTo,
'Referred-By': referredBy
}
@@ -101,7 +100,6 @@ class TaskSipRefer extends Task {
/* they may have only provided a phone number/user */
referTo = `sip:${referTo}@${host}`;
}
else this.referToIsUri = true;
if (!referredBy) {
/* default */
referredBy = cs.req?.callingNumber || dlg.local.uri;

View File

@@ -98,10 +98,7 @@
"properties": {
"url": "string|array",
"loop": "number|string",
"earlyMedia": "boolean",
"seekOffset": "number|string",
"timeoutSecs": "number|string",
"actionHook": "object|string"
"earlyMedia": "boolean"
},
"required": [
"url"
@@ -341,7 +338,6 @@
"call_hook": "object|string",
"call_status_hook": "object|string",
"from": "string",
"fromHost": "string",
"speech_synthesis_vendor": "string",
"speech_synthesis_voice": "string",
"speech_synthesis_language": "string",
@@ -388,7 +384,6 @@
"enum": ["GET", "POST"]
},
"headers": "object",
"from": "#dialFrom",
"name": "string",
"number": "string",
"sipUri": "string",
@@ -402,14 +397,6 @@
"type"
]
},
"dialFrom": {
"properties": {
"user": "string",
"host": "string"
},
"required": [
]
},
"auth": {
"properties": {
"username": "string",
@@ -435,8 +422,7 @@
"gender": {
"type": "string",
"enum": ["MALE", "FEMALE", "NEUTRAL"]
},
"azureServiceEndpoint": "string"
}
},
"required": [
"vendor"
@@ -508,10 +494,8 @@
"requestSnr": "boolean",
"initialSpeechTimeoutMs": "number",
"azureServiceEndpoint": "string",
"azureSttEndpointId": "string",
"asrDtmfTerminationDigit": "string",
"asrTimeout": "number",
"audioLogging": "boolean"
"asrTimeout": "number"
},
"required": [
"vendor"

View File

@@ -1,5 +1,5 @@
const Emitter = require('events');
const uuidv4 = require('uuid-random');
const { v4: uuidv4 } = require('uuid');
const debug = require('debug')('jambonz:feature-server');
const assert = require('assert');
const {TaskPreconditions} = require('../utils/constants');
@@ -336,9 +336,6 @@ class Task extends Emitter {
}
required = required.filter((item) => item !== dKey);
}
else if (dKey === '_') {
/* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */
}
else throw new Error(`${name}: unknown property ${dKey}`);
}
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);

View File

@@ -54,8 +54,6 @@ class TaskTranscribe extends Task {
this.requestSnr = recognizer.requestSnr || false;
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
this.azureSttEndpointId = recognizer.azureSttEndpointId;
this.azureAudioLogging = recognizer.audioLogging;
}
get name() { return TaskName.Transcribe; }
@@ -76,9 +74,6 @@ class TaskTranscribe extends Task {
this.logger.debug({altLanguages: this.altLanguages},
'Gather:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation) {
this.punctuation = cs.globalSttPunctuation;
}
this.ep = ep;
this.ep2 = ep2;
@@ -91,6 +86,7 @@ class TaskTranscribe extends Task {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor
@@ -163,7 +159,7 @@ class TaskTranscribe extends Task {
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
if (this.vendor === 'google') {
this.bugname = 'google_transcribe';
this.bugname = 'google_trancribe';
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
@@ -175,7 +171,6 @@ class TaskTranscribe extends Task {
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
else if (this[arr[0]] === false) opts[arr[1]] = false;
});
if (this.hints.length > 0) {
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
@@ -201,7 +196,7 @@ class TaskTranscribe extends Task {
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
}
else if (this.vendor === 'aws') {
this.bugname = 'aws_transcribe';
this.bugname = 'aws_trancribe';
[
['diarization', 'AWS_SHOW_SPEAKER_LABEL'],
['identifyChannels', 'AWS_ENABLE_CHANNEL_IDENTIFICATION']
@@ -233,24 +228,16 @@ class TaskTranscribe extends Task {
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
}
else if (this.vendor === 'microsoft') {
this.bugname = 'azure_transcribe';
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
this.bugname = 'azure_trancribe';
Object.assign(opts, {
'AZURE_SUBSCRIPTION_KEY': api_key,
'AZURE_REGION': region
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
'AZURE_REGION': this.sttCredentials.region
});
if (this.azureSttEndpointId) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
}
else if (use_custom_stt && custom_stt_endpoint) {
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
}
if (this.hints && this.hints.length > 0) {
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
}
if (this.altLanguages.length > 0) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
else opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
if (this.azureAudioLogging) opts.AZURE_AUDIO_LOGGING = 1;
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
@@ -315,7 +302,7 @@ class TaskTranscribe extends Task {
const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3};
this.cs.requestor.request('verb:hook', this.transcriptionHook,
Object.assign({speech: evt}, this.cs.callInfo), httpHeaders)
Object.assign({speech: evt}, this.cs.callInfo.toJSON()), httpHeaders)
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
}
if (this.parentTask) {

View File

@@ -193,6 +193,7 @@ module.exports = (logger) => {
task.emit(AmdEvents.Error, err);
logger.error(err, 'amd:_startTranscribing error');
writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
vendor: vendor,
@@ -273,46 +274,26 @@ module.exports = (logger) => {
amd
.on(AmdEvents.NoSpeechDetected, (evt) => {
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
ep.stopTranscription({vendor, bugname});
})
.on(AmdEvents.HumanDetected, (evt) => {
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
ep.stopTranscription({vendor, bugname});
})
.on(AmdEvents.MachineDetected, (evt) => {
task.emit('amd', {type: AmdEvents.MachineDetected, ...evt});
})
.on(AmdEvents.DecisionTimeout, (evt) => {
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
ep.stopTranscription({vendor, bugname});
})
.on(AmdEvents.ToneTimeout, (evt) => {
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
try {
ep.connected && ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
} catch (err) {
logger.info({err}, 'Error stopping avmd');
}
ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
})
.on(AmdEvents.MachineStoppedSpeaking, () => {
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
ep.stopTranscription({vendor, bugname});
});
/* start transcribing, and also listening for beep */

View File

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

View File

@@ -5,7 +5,7 @@ const timeSeries = require('@jambonz/time-series');
let alerter ;
class BaseRequestor extends Emitter {
constructor(logger, account_sid, hook, secret) {
constructor(logger, service_provider_sid, account_sid, hook, secret) {
super();
assert(typeof hook === 'object');
@@ -15,6 +15,7 @@ class BaseRequestor extends Emitter {
this.username = hook.username;
this.password = hook.password;
this.secret = secret;
this.service_provider_sid = service_provider_sid;
this.account_sid = account_sid;
const {stats} = require('../../').srf.locals;

View File

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

View File

@@ -23,30 +23,23 @@ AND vc.name = ?`;
const speechMapper = (cred) => {
const {credential, ...obj} = cred;
try {
if ('google' === obj.vendor) {
obj.service_key = decrypt(credential);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = o.secret_access_key;
obj.aws_region = o.aws_region;
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.region = o.region;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
} catch (err) {
if ('google' === obj.vendor) {
obj.service_key = decrypt(credential);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = o.secret_access_key;
obj.aws_region = o.aws_region;
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.region = o.region;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
return obj;
};

View File

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

View File

@@ -18,8 +18,8 @@ function basicAuth(username, password) {
class HttpRequestor extends BaseRequestor {
constructor(logger, account_sid, hook, secret) {
super(logger, account_sid, hook, secret);
constructor(logger, service_provider_sid, account_sid, hook, secret) {
super(logger, service_provider_sid, account_sid, hook, secret);
this.method = hook.method || 'POST';
this.authHeader = basicAuth(hook.username, hook.password);
@@ -28,11 +28,9 @@ class HttpRequestor extends BaseRequestor {
assert(['GET', 'POST'].includes(this.method));
const u = this._parsedUrl = parseUrl(this.url);
if (u.port) this._baseUrl = `${u.protocol}://${u.resource}:${u.port}`;
else this._baseUrl = `${u.protocol}://${u.resource}`;
this._protocol = u.protocol;
this._baseUrl = `${u.protocol}://${u.resource}`;
this._resource = u.resource;
this._port = u.port;
this._protocol = u.protocol;
this._search = u.search;
this._usePools = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
@@ -51,10 +49,7 @@ class HttpRequestor extends BaseRequestor {
this.logger.debug(`HttpRequestor:created pool for ${this._baseUrl}`);
}
}
else {
if (u.port) this.client = new Client(`${u.protocol}://${u.resource}:${u.port}`);
else this.client = new Client(`${u.protocol}://${u.resource}`);
}
else this.client = new Client(`${u.protocol}://${u.resource}`);
}
get baseUrl() {
@@ -100,14 +95,13 @@ class HttpRequestor extends BaseRequestor {
}
else {
const u = parseUrl(url);
if (u.resource === this._resource && u.port === this._port && u.protocol === this._protocol) {
if (u.resource === this._resource && u.protocol === this._protocol) {
client = this.client;
path = u.pathname;
query = u.query;
}
else {
if (u.port) client = newClient = new Client(`${u.protocol}://${u.resource}:${u.port}`);
else client = newClient = new Client(`${u.protocol}://${u.resource}`);
client = newClient = new Client(`${u.protocol}://${u.resource}`);
path = u.pathname;
query = u.query;
}
@@ -148,7 +142,7 @@ class HttpRequestor extends BaseRequestor {
this.logger.error({err, baseUrl: this.baseUrl, url},
'web callback returned unexpected error');
}
let opts = {account_sid: this.account_sid};
let opts = {account_sid: this.account_sid, service_provider_sid: this.service_provider_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
}

View File

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

View File

@@ -1,50 +0,0 @@
const assert = require('assert');
const timeSeries = require('@jambonz/time-series');
let alerter ;
function isAbsoluteUrl(u) {
return typeof u === 'string' &&
u.startsWith('https://') || u.startsWith('http://');
}
class Requestor {
constructor(logger, account_sid, hook, secret) {
assert(typeof hook === 'object');
this.logger = logger;
this.url = hook.url;
this.method = hook.method || 'POST';
this.username = hook.username;
this.password = hook.password;
this.secret = secret;
this.account_sid = account_sid;
assert(isAbsoluteUrl(this.url));
assert(['GET', 'POST'].includes(this.method));
const {stats} = require('../../').srf.locals;
this.stats = stats;
if (!alerter) {
alerter = timeSeries(logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50,
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
}
}
get Alerter() {
if (!alerter) {
alerter = timeSeries(this.logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50,
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
}
return alerter;
}
}
module.exports = Requestor;

View File

@@ -1,5 +1,5 @@
const assert = require('assert');
const uuidv4 = require('uuid-random');
const { v4: uuidv4 } = require('uuid');
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants');
const Emitter = require('events');
const debug = require('debug')('jambonz:feature-server');

View File

@@ -1,5 +1,5 @@
const xmlParser = require('xml2js').parseString;
const uuidv4 = require('uuid-random');
const { v4: uuidv4 } = require('uuid');
const parseUri = require('drachtio-srf').parseUri;
const transform = require('sdp-transform');
const debug = require('debug')('jambonz:feature-server');

View File

@@ -9,8 +9,8 @@ const MAX_RECONNECTS = 5;
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
class WsRequestor extends BaseRequestor {
constructor(logger, account_sid, hook, secret) {
super(logger, account_sid, hook, secret);
constructor(logger, service_provider_sid, account_sid, hook, secret) {
super(logger, service_provider_sid, account_sid, hook, secret);
this.connections = 0;
this.messagesInFlight = new Map();
this.maliciousClient = false;
@@ -54,11 +54,7 @@ class WsRequestor extends BaseRequestor {
/* if we have an absolute url, and it is http then do a standard webhook */
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
const requestor = new HttpRequestor(this.logger, this.account_sid, {url: hook}, this.secret);
if (type === 'session:redirect') {
this.close();
this.emit('handover', requestor);
}
const requestor = new HttpRequestor(this.logger, this.service_provider_sid, this.account_sid, hook, this.secret);
return requestor.request(type, hook, params, httpHeaders);
}
@@ -73,7 +69,7 @@ class WsRequestor extends BaseRequestor {
this.connectInProgress = true;
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection`);
if (this.connections >= MAX_RECONNECTS) {
return Promise.reject(`max attempts connecting to ${this.url}`);
throw new Error(`max attempts connecting to ${this.url}`);
}
try {
const startAt = process.hrtime();
@@ -83,7 +79,7 @@ class WsRequestor extends BaseRequestor {
} catch (err) {
this.logger.info({url, err}, 'WsRequestor:request - failed connecting');
this.connectInProgress = false;
return Promise.reject(err);
throw err;
}
}
assert(this.ws);
@@ -142,7 +138,7 @@ class WsRequestor extends BaseRequestor {
success: (response) => {
clearTimeout(timer);
const rtt = this._roundTrip(startAt);
this.logger.debug({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
this.logger.info({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']);
resolve(response);
},
@@ -191,7 +187,7 @@ class WsRequestor extends BaseRequestor {
followRedirects: true,
maxRedirects: 2,
handshakeTimeout,
maxPayload: process.env.JAMBONES_WS_MAX_PAYLOAD ? parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024,
maxPayload: 8096,
};
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
@@ -290,7 +286,7 @@ class WsRequestor extends BaseRequestor {
const obj = JSON.parse(content);
const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
//this.logger.debug({obj}, 'WsRequestor:request websocket: received');
this.logger.debug({obj}, 'WsRequestor:request websocket: received');
assert.ok(type, 'type property not supplied');
switch (type) {
@@ -327,7 +323,7 @@ class WsRequestor extends BaseRequestor {
_recvCommand(msgid, command, call_sid, queueCommand, data) {
// TODO: validate command
this.logger.debug({msgid, command, call_sid, queueCommand, data}, 'received command');
this.logger.info({msgid, command, call_sid, queueCommand, data}, 'received command');
this.emit('command', {msgid, command, call_sid, queueCommand, data});
}
}

8118
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-feature-server",
"version": "v0.7.8",
"version": "v0.7.6",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -16,7 +16,9 @@
"type": "git",
"url": "https://github.com/jambonz/jambonz-feature-server.git"
},
"bugs": {},
"bugs": {
"url": "https://github.com/jambonz/jambonz-feature-server/issues"
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
@@ -24,11 +26,11 @@
"jslint": "eslint app.js lib"
},
"dependencies": {
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.6.3",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.2.5",
"@jambonz/time-series": "^0.2.0",
"@opentelemetry/api": "^1.1.0",
"@opentelemetry/exporter-jaeger": "^1.3.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
@@ -42,19 +44,20 @@
"bent": "^7.3.12",
"debug": "^4.3.4",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.16",
"drachtio-srf": "^4.5.21",
"express": "^4.18.2",
"drachtio-fsmrf": "^3.0.2",
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"helmet": "^5.1.0",
"ip": "^1.1.8",
"moment": "^2.29.4",
"parse-url": "^8.1.0",
"parse-url": "^7.0.2",
"pino": "^6.14.0",
"sdp-transform": "^2.14.1",
"short-uuid": "^4.2.0",
"to-snake-case": "^1.0.0",
"undici": "^5.11.0",
"uuid-random": "^1.3.2",
"verify-aws-sns-signature": "^0.1.0",
"undici": "^5.8.2",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.7",
"ws": "^8.8.0",
"xml2js": "^0.4.23"
},

View File

@@ -156,43 +156,3 @@ test('\'play\' tests multi links in array in conference', async(t) => {
t.error(err);
}
});
test('\'play\' tests with seekOffset and actionHook', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'silence_stream://5000',
seekOffset: 8000,
timeoutSecs: 2,
actionHook: '/customHook'
}
];
const waitHookVerbs = [];
const from = 'play_action_hook';
provisionCallHook(from, verbs)
provisionCustomHook(from, waitHookVerbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds');
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
t.ok(obj.body.reason === "playCompleted", "play: actionHook success received")
t.ok(obj.body.playback_seconds === "2", "playback_seconds: actionHook success received")
t.ok(obj.body.playback_milliseconds === "2048", "playback_milliseconds: actionHook success received")
t.ok(obj.body.playback_last_offset_pos === "16000", "playback_last_offset_pos: actionHook success received")
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -22,7 +22,7 @@ module.exports = (serviceName) => {
});
let exporter;
if (process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST || process.env.OTEL_EXPORTER_JAEGER_ENDPOINT) {
if (process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST) {
exporter = new JaegerExporter();
}
else if (process.env.OTEL_EXPORTER_ZIPKIN_URL) {