mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-03-30 03:49:10 +00:00
Compare commits
103 Commits
v0.7.4-rc2
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4855fec4f5 | ||
|
|
a11822609f | ||
|
|
8dd9bfbb74 | ||
|
|
cee21c3dbc | ||
|
|
e2753ca8a3 | ||
|
|
df302320ca | ||
|
|
83a3bb61fe | ||
|
|
c36b55303a | ||
|
|
60d7f0f31c | ||
|
|
b089bd4663 | ||
|
|
5c8f91c1c1 | ||
|
|
50f926bce4 | ||
|
|
2a61d21bff | ||
|
|
418baa20df | ||
|
|
eb418a42e9 | ||
|
|
57ba79f908 | ||
|
|
3a791a67b5 | ||
|
|
91108fa3ef | ||
|
|
36c723d8f0 | ||
|
|
bc59fc80c9 | ||
|
|
6960466afc | ||
|
|
09c2608114 | ||
|
|
f1c17e537d | ||
|
|
22dad4eed6 | ||
|
|
017bc39103 | ||
|
|
8b982f20d6 | ||
|
|
b6d0d4cb0e | ||
|
|
4636b487b4 | ||
|
|
fce40a47ce | ||
|
|
1fd94dce94 | ||
|
|
efba631282 | ||
|
|
2fc3febcf6 | ||
|
|
cc67132dfa | ||
|
|
f8e88f085f | ||
|
|
1050eb47cd | ||
|
|
4e2feda7f3 | ||
|
|
1685262658 | ||
|
|
6fc9a3567e | ||
|
|
fb33574861 | ||
|
|
d2732c9be6 | ||
|
|
294d38dcd1 | ||
|
|
7bdac328bf | ||
|
|
2e0f4b94dc | ||
|
|
53e5360ab3 | ||
|
|
8163c33462 | ||
|
|
7e9b6498c5 | ||
|
|
a1c59a5f25 | ||
|
|
52a2ce8a86 | ||
|
|
2738299524 | ||
|
|
e872d314b3 | ||
|
|
b1683bc294 | ||
|
|
78869e4bb8 | ||
|
|
ba994af012 | ||
|
|
e579514321 | ||
|
|
baed1b0eac | ||
|
|
b9dfecceff | ||
|
|
c7c99f45a4 | ||
|
|
67d18b26ff | ||
|
|
53cfbc1f56 | ||
|
|
da35449c16 | ||
|
|
83c015c839 | ||
|
|
f1f21fb23b | ||
|
|
c88ead7f71 | ||
|
|
b41c1ffb91 | ||
|
|
cc98f40d44 | ||
|
|
11035264ec | ||
|
|
5389083107 | ||
|
|
9657017669 | ||
|
|
b532a49e45 | ||
|
|
97d7a60994 | ||
|
|
6dbbbf8c9e | ||
|
|
4ea9707e4e | ||
|
|
e20c472bd2 | ||
|
|
f2421bb3dd | ||
|
|
18a141edc6 | ||
|
|
30a4e0e6a3 | ||
|
|
924627a50c | ||
|
|
d76a5e6efb | ||
|
|
cdfb1fff1e | ||
|
|
5c1501b6c7 | ||
|
|
161e67ece5 | ||
|
|
65b04d7cbf | ||
|
|
8a641f1d9a | ||
|
|
95188f59ec | ||
|
|
4e76077dc9 | ||
|
|
3c75d5a489 | ||
|
|
3164e1ea4e | ||
|
|
c9e3e97d53 | ||
|
|
ed157c6aee | ||
|
|
c395109966 | ||
|
|
7228594f79 | ||
|
|
28cde62d5d | ||
|
|
db6f56a068 | ||
|
|
9f757b439f | ||
|
|
35114b22d8 | ||
|
|
68b2ad526a | ||
|
|
b8ef1dba73 | ||
|
|
2ce902c00d | ||
|
|
8c00c89882 | ||
|
|
dcd6ddcbca | ||
|
|
9a71350875 | ||
|
|
1bca165fc1 | ||
|
|
b94605127e |
13
app.js
13
app.js
@@ -124,17 +124,4 @@ function handle(signal) {
|
||||
srf.locals.disabled = true;
|
||||
}
|
||||
|
||||
if (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS) {
|
||||
const {clearFiles} = require('./lib/utils/cron-jobs');
|
||||
|
||||
/* cleanup orphaned files or channels every so often */
|
||||
setInterval(async() => {
|
||||
try {
|
||||
await clearFiles();
|
||||
} catch (err) {
|
||||
logger.error({err}, 'app.js: error clearing files');
|
||||
}
|
||||
}, 1000 * 60 * (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS || 60));
|
||||
}
|
||||
|
||||
module.exports = {srf, logger, disconnect};
|
||||
|
||||
@@ -6,8 +6,7 @@ const {CallDirection, CallStatus} = require('../../utils/constants');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const SipError = require('drachtio-srf').SipError;
|
||||
const sysError = require('./error');
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
const WsRequestor = require('../../utils/ws-requestor');
|
||||
const Requestor = require('../../utils/requestor');
|
||||
const dbUtils = require('../../utils/db-utils');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
@@ -72,17 +71,6 @@ router.post('/', async(req, res) => {
|
||||
break;
|
||||
}
|
||||
|
||||
if (target.type === 'phone' && target.trunk) {
|
||||
const {lookupCarrier} = dbUtils(this.logger, srf);
|
||||
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* create endpoint for outdial */
|
||||
const ms = getFreeswitch();
|
||||
if (!ms) throw new Error('no available Freeswitch for outbound call creation');
|
||||
@@ -116,16 +104,11 @@ 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) {
|
||||
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook,
|
||||
account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
app.requestor = new Requestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||
if (app.call_status_hook) {
|
||||
app.notifier = new Requestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
||||
}
|
||||
else app.notifier = {request: () => {}};
|
||||
|
||||
/* now launch the outdial */
|
||||
try {
|
||||
@@ -139,7 +122,6 @@ router.post('/', async(req, res) => {
|
||||
res.status(500).send('Call Failure');
|
||||
return;
|
||||
}
|
||||
inviteReq.srf = srf;
|
||||
/* ok our outbound INVITE is in flight */
|
||||
|
||||
const tasks = [restDial];
|
||||
@@ -171,11 +153,7 @@ router.post('/', async(req, res) => {
|
||||
}
|
||||
});
|
||||
connectStream(dlg.remote.sdp);
|
||||
cs.emit('callStatusChange', {
|
||||
callStatus: CallStatus.InProgress,
|
||||
sipStatus: 200,
|
||||
sipReason: 'OK'
|
||||
});
|
||||
cs.emit('callStatusChange', {callStatus: CallStatus.InProgress, sipStatus: 200});
|
||||
restDial.emit('callStatus', 200);
|
||||
restDial.emit('connect', dlg);
|
||||
}
|
||||
@@ -186,18 +164,10 @@ router.post('/', async(req, res) => {
|
||||
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
|
||||
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
|
||||
else console.log(`REST outdial failed with ${err.status}`);
|
||||
if (cs) cs.emit('callStatusChange', {
|
||||
callStatus,
|
||||
sipStatus: err.status,
|
||||
sipReason: err.reason
|
||||
});
|
||||
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: err.status});
|
||||
}
|
||||
else {
|
||||
if (cs) cs.emit('callStatusChange', {
|
||||
callStatus,
|
||||
sipStatus: 500,
|
||||
sipReason: 'Internal Server Error'
|
||||
});
|
||||
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: 500});
|
||||
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
|
||||
else console.error(err);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
const WsRequestor = require('../../utils/ws-requestor');
|
||||
const Requestor = require('../../utils/requestor');
|
||||
const CallInfo = require('../../session/call-info');
|
||||
const {CallDirection} = require('../../utils/constants');
|
||||
const SmsSession = require('../../session/sms-call-session');
|
||||
@@ -19,17 +18,7 @@ router.post('/:partner', async(req, res) => {
|
||||
const app = req.body.app;
|
||||
const account = await lookupAccountBySid(app.accountSid);
|
||||
const hook = app.messaging_hook;
|
||||
let requestor;
|
||||
|
||||
if ('WS' === hook?.method) {
|
||||
app.requestor = new WsRequestor(logger, account.account_sid, hook, account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account.account_sid, hook, account.webhook_secret);
|
||||
app.notifier = {request: () => {}};
|
||||
}
|
||||
|
||||
const requestor = new Requestor(logger, account.account_sid, hook, account.webhook_secret);
|
||||
const payload = {
|
||||
carrier: req.params.partner,
|
||||
messageSid: app.messageSid,
|
||||
@@ -44,7 +33,7 @@ router.post('/:partner', async(req, res) => {
|
||||
res.status(200).json({sid: req.body.messageSid});
|
||||
|
||||
try {
|
||||
tasks = await requestor.request('session:new', hook, payload);
|
||||
tasks = await requestor.request(hook, payload);
|
||||
logger.info({tasks}, 'response from incoming SMS webhook');
|
||||
} catch (err) {
|
||||
logger.error({err, hook}, 'Error sending incoming SMS message');
|
||||
|
||||
@@ -12,9 +12,6 @@ function retrieveCallSession(callSid, opts) {
|
||||
throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated');
|
||||
}
|
||||
const cs = sessionTracker.get(callSid);
|
||||
if (!cs) {
|
||||
throw new DbErrorUnprocessableRequest('call session is gone');
|
||||
}
|
||||
|
||||
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
|
||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
||||
@@ -48,18 +45,8 @@ router.post('/:callSid', async(req, res) => {
|
||||
logger.info(`updateCall: callSid not found ${callSid}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
if (req.body.sip_request) {
|
||||
const response = await cs.updateCall(req.body, callSid);
|
||||
res.status(200).json({
|
||||
status: response.status,
|
||||
reason: response.reason
|
||||
});
|
||||
}
|
||||
else {
|
||||
res.sendStatus(202);
|
||||
cs.updateCall(req.body, callSid);
|
||||
}
|
||||
res.sendStatus(202);
|
||||
cs.updateCall(req.body, callSid);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const {CallDirection} = require('./utils/constants');
|
||||
const CallInfo = require('./session/call-info');
|
||||
const HttpRequestor = require('./utils/http-requestor');
|
||||
const WsRequestor = require('./utils/ws-requestor');
|
||||
const Requestor = require('./utils/requestor');
|
||||
const makeTask = require('./tasks/make_task');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const normalizeJambones = require('./utils/normalize-jambones');
|
||||
@@ -106,7 +105,7 @@ module.exports = function(srf, logger) {
|
||||
}
|
||||
else {
|
||||
const uri = parseUri(req.uri);
|
||||
const arr = /context-(.*)/.exec(uri?.user);
|
||||
const arr = /context-(.*)/.exec(uri.user);
|
||||
if (arr) {
|
||||
// this is a transfer from another feature server
|
||||
const {retrieveKey, deleteKey} = srf.locals.dbHelpers;
|
||||
@@ -143,18 +142,10 @@ 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).
|
||||
*/
|
||||
if ('WS' === app.call_hook?.method ||
|
||||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
|
||||
app.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
|
||||
app.notifier = app.requestor;
|
||||
app.call_hook.method = 'WS';
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
|
||||
accountInfo.account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
}
|
||||
app.requestor = new Requestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new Requestor(logger, account_sid, app.call_status_hook,
|
||||
accountInfo.account.webhook_secret);
|
||||
else app.notifier = {request: () => {}};
|
||||
|
||||
req.locals.application = app;
|
||||
const obj = Object.assign({}, app);
|
||||
@@ -185,16 +176,15 @@ module.exports = function(srf, logger) {
|
||||
return next();
|
||||
}
|
||||
/* retrieve the application to execute for this inbound call */
|
||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
|
||||
const params = Object.assign(app.call_hook.method === 'POST' ? {sip: req.msg} : {},
|
||||
req.locals.callInfo);
|
||||
const json = await app.requestor.request('session:new', app.call_hook, params);
|
||||
const json = await app.requestor.request(app.call_hook, params);
|
||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err?.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}});
|
||||
app.requestor.close();
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err.message}});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,25 +30,15 @@ class AdultingCallSession extends CallSession {
|
||||
return this.sd.dlg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this is not an error. It is only here to avoid an assert ("no setter for dlg")
|
||||
* when there is a call in Session:_clearResources to null out dlg and ep
|
||||
*/
|
||||
set dlg(newDlg) {}
|
||||
|
||||
get ep() {
|
||||
return this.sd.ep;
|
||||
}
|
||||
|
||||
/* see note above */
|
||||
set ep(newEp) {}
|
||||
|
||||
get callSid() {
|
||||
return this.callInfo.callSid;
|
||||
}
|
||||
|
||||
_callerHungup() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = AdultingCallSession;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const {CallDirection, CallStatus} = require('../utils/constants');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
/**
|
||||
* @classdesc Represents the common information for all calls
|
||||
* that is provided in call status webhooks
|
||||
@@ -8,7 +9,6 @@ const { v4: uuidv4 } = require('uuid');
|
||||
class CallInfo {
|
||||
constructor(opts) {
|
||||
let from ;
|
||||
let srf;
|
||||
this.direction = opts.direction;
|
||||
if (opts.req) {
|
||||
const u = opts.req.getParsedHeader('from');
|
||||
@@ -19,7 +19,6 @@ class CallInfo {
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
// inbound call
|
||||
const {app, req} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = req.locals.callSid,
|
||||
this.accountSid = app.account_sid,
|
||||
this.applicationSid = app.application_sid;
|
||||
@@ -27,7 +26,6 @@ class CallInfo {
|
||||
this.to = req.calledNumber;
|
||||
this.callId = req.get('Call-ID');
|
||||
this.sipStatus = 100;
|
||||
this.sipReason = 'Trying';
|
||||
this.callStatus = CallStatus.Trying;
|
||||
this.originatingSipIp = req.get('X-Forwarded-For');
|
||||
this.originatingSipTrunkName = req.get('X-Originating-Carrier');
|
||||
@@ -35,7 +33,6 @@ class CallInfo {
|
||||
else if (opts.parentCallInfo) {
|
||||
// outbound call that is a child of an existing call
|
||||
const {req, parentCallInfo, to, callSid} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = callSid || uuidv4();
|
||||
this.parentCallSid = parentCallInfo.callSid;
|
||||
this.accountSid = parentCallInfo.accountSid;
|
||||
@@ -46,12 +43,10 @@ class CallInfo {
|
||||
this.callId = req.get('Call-ID');
|
||||
this.callStatus = CallStatus.Trying,
|
||||
this.sipStatus = 100;
|
||||
this.sipReason = 'Trying';
|
||||
}
|
||||
else if (this.direction === CallDirection.None) {
|
||||
// outbound SMS
|
||||
const {messageSid, accountSid, applicationSid, res} = opts;
|
||||
srf = res.srf;
|
||||
this.messageSid = messageSid;
|
||||
this.accountSid = accountSid;
|
||||
this.applicationSid = applicationSid;
|
||||
@@ -60,23 +55,16 @@ class CallInfo {
|
||||
else {
|
||||
// outbound call triggered by REST
|
||||
const {req, callSid, accountSid, applicationSid, to, tag} = opts;
|
||||
srf = req.srf;
|
||||
this.callSid = callSid;
|
||||
this.accountSid = accountSid;
|
||||
this.applicationSid = applicationSid;
|
||||
this.callStatus = CallStatus.Trying,
|
||||
this.callId = req.get('Call-ID');
|
||||
this.sipStatus = 100;
|
||||
this.sipReason = 'Trying';
|
||||
this.from = from || req.callingNumber;
|
||||
this.to = to;
|
||||
if (tag) this._customerData = tag;
|
||||
}
|
||||
|
||||
this.localSipAddress = srf.locals.localSipAddress;
|
||||
if (srf.locals.publicIp) {
|
||||
this.publicIp = srf.locals.publicIp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,10 +72,9 @@ class CallInfo {
|
||||
* @param {string} callStatus - current call status
|
||||
* @param {number} sipStatus - current sip status
|
||||
*/
|
||||
updateCallStatus(callStatus, sipStatus, sipReason) {
|
||||
updateCallStatus(callStatus, sipStatus) {
|
||||
this.callStatus = callStatus;
|
||||
if (sipStatus) this.sipStatus = sipStatus;
|
||||
if (sipReason) this.sipReason = sipReason;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,12 +97,10 @@ class CallInfo {
|
||||
to: this.to,
|
||||
callId: this.callId,
|
||||
sipStatus: this.sipStatus,
|
||||
sipReason: this.sipReason,
|
||||
callStatus: this.callStatus,
|
||||
callerId: this.callerId,
|
||||
accountSid: this.accountSid,
|
||||
applicationSid: this.applicationSid,
|
||||
fsSipAddress: this.localSipAddress
|
||||
applicationSid: this.applicationSid
|
||||
};
|
||||
['parentCallSid', 'originatingSipIp', 'originatingSipTrunkName'].forEach((prop) => {
|
||||
if (this[prop]) obj[prop] = this[prop];
|
||||
@@ -125,13 +110,6 @@ class CallInfo {
|
||||
if (this._customerData) {
|
||||
Object.assign(obj, {customerData: this._customerData});
|
||||
}
|
||||
|
||||
if (process.env.JAMBONES_API_BASE_URL) {
|
||||
Object.assign(obj, {apiBaseUrl: process.env.JAMBONES_API_BASE_URL});
|
||||
}
|
||||
if (this.publicIp) {
|
||||
Object.assign(obj, {fsPublicIp: this.publicIp});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ const sessionTracker = require('./session-tracker');
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const listTaskNames = require('../utils/summarize-tasks');
|
||||
const HttpRequestor = require('../utils/http-requestor');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
const Requestor = require('../utils/requestor');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
||||
|
||||
@@ -49,7 +48,6 @@ class CallSession extends Emitter {
|
||||
this.taskIdx = 0;
|
||||
this.stackIdx = 0;
|
||||
this.callGone = false;
|
||||
this.notifiedComplete = false;
|
||||
|
||||
this.tmpFiles = new Set();
|
||||
|
||||
@@ -63,8 +61,6 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
this._pool = srf.locals.dbHelpers.pool;
|
||||
|
||||
this.requestor.on('command', this._onCommand.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,27 +113,18 @@ class CallSession extends Emitter {
|
||||
get speechSynthesisVendor() {
|
||||
return this.application.speech_synthesis_vendor;
|
||||
}
|
||||
set speechSynthesisVendor(vendor) {
|
||||
this.application.speech_synthesis_vendor = vendor;
|
||||
}
|
||||
/**
|
||||
* default voice to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
get speechSynthesisVoice() {
|
||||
return this.application.speech_synthesis_voice;
|
||||
}
|
||||
set speechSynthesisVoice(voice) {
|
||||
this.application.speech_synthesis_voice = voice;
|
||||
}
|
||||
/**
|
||||
* default language to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
get speechSynthesisLanguage() {
|
||||
return this.application.speech_synthesis_language;
|
||||
}
|
||||
set speechSynthesisLanguage(language) {
|
||||
this.application.speech_synthesis_language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* default vendor to use for speech recognition if not provided in the app
|
||||
@@ -145,18 +132,12 @@ class CallSession extends Emitter {
|
||||
get speechRecognizerVendor() {
|
||||
return this.application.speech_recognizer_vendor;
|
||||
}
|
||||
set speechRecognizerVendor(vendor) {
|
||||
this.application.speech_recognizer_vendor = vendor;
|
||||
}
|
||||
/**
|
||||
* default language to use for speech recognition if not provided in the app
|
||||
*/
|
||||
get speechRecognizerLanguage() {
|
||||
return this.application.speech_recognizer_language;
|
||||
}
|
||||
set speechRecognizerLanguage(language) {
|
||||
this.application.speech_recognizer_language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* indicates whether the call currently in progress
|
||||
@@ -222,46 +203,6 @@ class CallSession extends Emitter {
|
||||
return this.memberId && this.confName && this.confUuid;
|
||||
}
|
||||
|
||||
get isBotModeEnabled() {
|
||||
return this.backgroundGatherTask;
|
||||
}
|
||||
|
||||
async enableBotMode(gather) {
|
||||
try {
|
||||
const t = normalizeJambones(this.logger, [gather]);
|
||||
this.backgroundGatherTask = makeTask(this.logger, t[0]);
|
||||
this.backgroundGatherTask
|
||||
.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);
|
||||
this.backgroundGatherTask.exec(this, resources)
|
||||
.then(() => {
|
||||
this.logger.info('CallSession:enableBotMode: gather completed');
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask = null;
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.info({err}, 'CallSession:enableBotMode: gather threw error');
|
||||
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask = null;
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.info({err, gather}, 'CallSession:enableBotMode - Error creating gather task');
|
||||
}
|
||||
}
|
||||
disableBotMode() {
|
||||
if (this.backgroundGatherTask) {
|
||||
try {
|
||||
this.backgroundGatherTask.removeAllListeners();
|
||||
this.backgroundGatherTask.kill();
|
||||
} catch (err) {}
|
||||
this.backgroundGatherTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
setConferenceDetails(memberId, confName, confUuid) {
|
||||
assert(!this.memberId && !this.confName && !this.confUuid);
|
||||
assert (memberId && confName && confUuid);
|
||||
@@ -347,7 +288,6 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
async exec() {
|
||||
this.logger.info({tasks: listTaskNames(this.tasks)}, `CallSession:exec starting ${this.tasks.length} tasks`);
|
||||
|
||||
while (this.tasks.length && !this.callGone) {
|
||||
const taskNum = ++this.taskIdx;
|
||||
const stackNum = this.stackIdx;
|
||||
@@ -356,19 +296,12 @@ class CallSession extends Emitter {
|
||||
try {
|
||||
const resources = await this._evaluatePreconditions(task);
|
||||
this.currentTask = task;
|
||||
if (TaskName.Gather === task.name && this.isBotModeEnabled) {
|
||||
const timeout = task.timeout;
|
||||
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
|
||||
this.backgroundGatherTask.updateTimeout(timeout);
|
||||
}
|
||||
else {
|
||||
await task.exec(this, resources);
|
||||
}
|
||||
await task.exec(this, resources);
|
||||
this.currentTask = null;
|
||||
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
} catch (err) {
|
||||
this.currentTask = null;
|
||||
if (err.message?.includes(BADPRECONDITIONS)) {
|
||||
if (err.message.includes(BADPRECONDITIONS)) {
|
||||
this.logger.info(`CallSession:exec task #${stackNum}:${taskNum}: ${task.name}: ${err.message}`);
|
||||
}
|
||||
else {
|
||||
@@ -376,16 +309,6 @@ class CallSession extends Emitter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === this.tasks.length && this.hasStableDialog && this.requestor instanceof WsRequestor) {
|
||||
try {
|
||||
await this._awaitCommandsOrHangup();
|
||||
if (!this.hasStableDialog || this.callGone) break;
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// all done - cleanup
|
||||
@@ -444,11 +367,6 @@ class CallSession extends Emitter {
|
||||
this.currentTask.kill(this);
|
||||
this.currentTask = null;
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
this.requestor && this.requestor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -485,42 +403,29 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
async _lccCallHook(opts) {
|
||||
const webhooks = [];
|
||||
let sd, tasks, childTasks;
|
||||
|
||||
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()));
|
||||
let sd;
|
||||
if (opts.call_hook) webhooks.push(this.requestor.request(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(opts.child_call_hook, sd.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()));
|
||||
}
|
||||
}
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
if (opts.call_hook) {
|
||||
tasks = tasks1;
|
||||
if (opts.child_call_hook) childTasks = tasks2;
|
||||
}
|
||||
else childTasks = tasks1;
|
||||
}
|
||||
else if (opts.parent_call || opts.child_call) {
|
||||
const {parent_call, child_call} = opts;
|
||||
assert.ok(!parent_call || Array.isArray(parent_call), 'CallSession:_lccCallHook - parent_call must be an array');
|
||||
assert.ok(!child_call || Array.isArray(child_call), 'CallSession:_lccCallHook - child_call must be an array');
|
||||
tasks = parent_call;
|
||||
childTasks = child_call;
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
let tasks, childTasks;
|
||||
if (opts.call_hook) {
|
||||
tasks = tasks1;
|
||||
if (opts.child_call_hook) childTasks = tasks2;
|
||||
}
|
||||
else childTasks = tasks1;
|
||||
|
||||
if (childTasks) {
|
||||
const {parentLogger} = this.srf.locals;
|
||||
const childLogger = parentLogger.child({callId: this.callId, callSid: sd.callSid});
|
||||
const t = normalizeJambones(childLogger, childTasks).map((tdata) => makeTask(childLogger, tdata));
|
||||
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list for child call');
|
||||
|
||||
// TODO: if using websockets api, we need a new websocket for the adulting session..
|
||||
const cs = await sd.doAdulting({
|
||||
logger: childLogger,
|
||||
application: this.application,
|
||||
@@ -568,7 +473,7 @@ class CallSession extends Emitter {
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
|
||||
}
|
||||
|
||||
async _lccConfHoldStatus(opts) {
|
||||
async _lccConfHoldStatus(callSid, opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
|
||||
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
|
||||
@@ -576,7 +481,7 @@ class CallSession extends Emitter {
|
||||
task.doConferenceHold(this, opts);
|
||||
}
|
||||
|
||||
async _lccConfMuteStatus(opts) {
|
||||
async _lccConfMuteStatus(callSid, opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
|
||||
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
|
||||
@@ -584,29 +489,6 @@ class CallSession extends Emitter {
|
||||
task.doConferenceMuteNonModerators(this, opts);
|
||||
}
|
||||
|
||||
async _lccSipRequest(opts) {
|
||||
const {sip_request} = opts;
|
||||
const {method, content_type, content, headers = {}} = sip_request;
|
||||
if (!this.hasStableDialog) {
|
||||
this.logger.info('CallSession:_lccSipRequest - invalid command as we do not have a stable call');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await this.dlg.request({
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': content_type,
|
||||
'Content': content
|
||||
}
|
||||
});
|
||||
this.logger.debug({res}, `CallSession:_lccSipRequest got response to ${method}`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `CallSession:_lccSipRequest - error sending ${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- whisper to one party or the other on a call
|
||||
* @param {array} opts - array of play or say tasks
|
||||
@@ -677,14 +559,10 @@ class CallSession extends Emitter {
|
||||
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
|
||||
}
|
||||
else if (opts.conf_hold_status) {
|
||||
await this._lccConfHoldStatus(opts);
|
||||
await this._lccConfHoldStatus(callSid, opts);
|
||||
}
|
||||
else if (opts.conf_mute_status) {
|
||||
await this._lccConfMuteStatus(opts);
|
||||
}
|
||||
else if (opts.sip_request) {
|
||||
const res = await this._lccSipRequest(opts);
|
||||
return {status: res.status, reason: res.reason};
|
||||
await this._lccConfMuteStatus(callSid, opts);
|
||||
}
|
||||
|
||||
// whisper may be the only thing we are asked to do, or it may that
|
||||
@@ -725,67 +603,6 @@ class CallSession extends Emitter {
|
||||
this.taskIdx = 0;
|
||||
}
|
||||
|
||||
_onCommand({msgid, command, queueCommand, data}) {
|
||||
this.logger.info({msgid, command, queueCommand}, 'CallSession:_onCommand - received command');
|
||||
switch (command) {
|
||||
case 'redirect':
|
||||
if (Array.isArray(data)) {
|
||||
const t = normalizeJambones(this.logger, data)
|
||||
.map((tdata) => makeTask(this.logger, tdata));
|
||||
if (!queueCommand) {
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list');
|
||||
this.replaceApplication(t);
|
||||
}
|
||||
else {
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
|
||||
this.tasks.push(...t);
|
||||
this.logger.debug({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||
}
|
||||
}
|
||||
else this._lccCallHook(data);
|
||||
break;
|
||||
|
||||
case 'call:status':
|
||||
this._lccCallStatus(data);
|
||||
break;
|
||||
|
||||
case 'mute:status':
|
||||
this._lccMuteStatus(this.callSid, data);
|
||||
break;
|
||||
|
||||
case 'conf:mute-status':
|
||||
this._lccConfMuteStatus(data);
|
||||
break;
|
||||
|
||||
case 'conf:hold-status':
|
||||
this._lccConfHoldStatus(data);
|
||||
break;
|
||||
|
||||
case 'listen:status':
|
||||
this._lccListenStatus(data);
|
||||
break;
|
||||
|
||||
case 'whisper':
|
||||
this._lccWhisper(data, this.callSid);
|
||||
break;
|
||||
|
||||
case 'sip:request':
|
||||
this._lccSipRequest(data)
|
||||
.catch((err) => {
|
||||
this.logger.info({err, data}, `CallSession:_onCommand - error sending ${data.method}`);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
|
||||
}
|
||||
if (this.wakeupResolver) {
|
||||
this.logger.info('CallSession:_onCommand - got commands, waking up..');
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
|
||||
_evaluatePreconditions(task) {
|
||||
switch (task.preconditions) {
|
||||
case TaskPreconditions.None:
|
||||
@@ -850,11 +667,7 @@ 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._notifyCallStatusChange({
|
||||
callStatus: CallStatus.NoAnswer,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Request Terminated'
|
||||
});
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
|
||||
this._callReleased();
|
||||
}
|
||||
else {
|
||||
@@ -926,7 +739,6 @@ class CallSession extends Emitter {
|
||||
});
|
||||
}
|
||||
this.tmpFiles.clear();
|
||||
this.requestor && this.requestor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -964,10 +776,9 @@ class CallSession extends Emitter {
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.wrapDialog(this.dlg);
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.emit('callStatusChange', {sipStatus: 200, sipReason: 'OK', callStatus: CallStatus.InProgress});
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
|
||||
this.dlg.on('modify', this._onReinvite.bind(this));
|
||||
this.dlg.on('refer', this._onRefer.bind(this));
|
||||
|
||||
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
|
||||
}
|
||||
@@ -993,22 +804,6 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming REFER if we are in a dial task
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
_onRefer(req, res) {
|
||||
const task = this.currentTask;
|
||||
const sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
task.handleRefer(this, req, res);
|
||||
}
|
||||
else {
|
||||
res.send(501);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create and endpoint if we don't have one; otherwise simply return
|
||||
* the current media server and endpoint that are associated with this call
|
||||
@@ -1045,7 +840,7 @@ 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,
|
||||
this.queueEventHookRequestor = new Requestor(this.logger, this.accountSid,
|
||||
r[0], this.webhook_secret);
|
||||
this.queueEventHook = r[0];
|
||||
}
|
||||
@@ -1059,7 +854,7 @@ class CallSession extends Emitter {
|
||||
/* send webhook */
|
||||
const params = {...obj, ...this.callInfo.toJSON()};
|
||||
this.logger.info({accountSid: this.accountSid, params}, 'performQueueWebhook: sending webhook');
|
||||
this.queueEventHookRequestor.request('queue:status', this.queueEventHook, params)
|
||||
this.queueEventHookRequestor.request(this.queueEventHook, params)
|
||||
.catch((err) => {
|
||||
this.logger.info({err, accountSid: this.accountSid, obj}, 'Error sending queue notification event');
|
||||
});
|
||||
@@ -1148,10 +943,6 @@ class CallSession extends Emitter {
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('CallSession: call terminated by jambones');
|
||||
origDestroy();
|
||||
if (this.wakeupResolver) {
|
||||
this.wakeupResolver();
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1194,23 +985,17 @@ class CallSession extends Emitter {
|
||||
* @param {number} sipStatus - current sip status
|
||||
* @param {number} [duration] - duration of a completed call, in seconds
|
||||
*/
|
||||
_notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
if (this.callMoved) return;
|
||||
|
||||
/* race condition: we hang up at the same time as the caller */
|
||||
if (callStatus === CallStatus.Completed) {
|
||||
if (this.notifiedComplete) return;
|
||||
this.notifiedComplete = true;
|
||||
}
|
||||
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.notifier.request('call:status', this.call_status_hook, this.callInfo.toJSON());
|
||||
this.notifier.request(this.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
@@ -1220,23 +1005,6 @@ class CallSession extends Emitter {
|
||||
this.updateCallStatus(Object.assign({}, this.callInfo.toJSON()), this.serviceUrl)
|
||||
.catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
|
||||
_awaitCommandsOrHangup() {
|
||||
assert(!this.wakeupResolver);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.logger.info('_awaitCommandsOrHangup - waiting...');
|
||||
this.wakeupResolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_clearTasks(evt) {
|
||||
if (this.requestor instanceof WsRequestor) {
|
||||
this.logger.info({evt}, 'CallSession:_clearTasks on event from background gather');
|
||||
try {
|
||||
this.kill();
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallSession;
|
||||
|
||||
@@ -30,10 +30,6 @@ class ConfirmCallSession extends CallSession {
|
||||
_clearResources() {
|
||||
}
|
||||
|
||||
_callerHungup() {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = ConfirmCallSession;
|
||||
|
||||
@@ -24,19 +24,11 @@ class InboundCallSession extends CallSession {
|
||||
req.once('cancel', this._onCancel.bind(this));
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.Trying,
|
||||
sipStatus: 100,
|
||||
sipReason: 'Trying'
|
||||
});
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.NoAnswer,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Request Terminated'
|
||||
});
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
@@ -64,10 +56,7 @@ class InboundCallSession extends CallSession {
|
||||
_callerHungup() {
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Completed,
|
||||
duration
|
||||
});
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('InboundCallSession: caller hung up');
|
||||
this._callReleased();
|
||||
this.req.removeAllListeners('cancel');
|
||||
|
||||
@@ -22,11 +22,7 @@ class RestCallSession extends CallSession {
|
||||
this.ep = ep;
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.Trying,
|
||||
sipStatus: 100,
|
||||
sipReason: 'Trying'
|
||||
});
|
||||
this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
470
lib/tasks/cognigy/index.js
Normal file
470
lib/tasks/cognigy/index.js
Normal file
@@ -0,0 +1,470 @@
|
||||
const Task = require('../task');
|
||||
const {TaskName, TaskPreconditions} = require('../../utils/constants');
|
||||
const makeTask = require('../make_task');
|
||||
const { SocketClient } = require('@cognigy/socket-client');
|
||||
const SpeechConfig = require('./speech-config');
|
||||
const queue = require('queue');
|
||||
|
||||
const parseGallery = (obj = {}) => {
|
||||
const {_default} = obj;
|
||||
if (_default) {
|
||||
const {_gallery} = _default;
|
||||
if (_gallery) return _gallery.fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
const parseQuickReplies = (obj) => {
|
||||
const {_default} = obj;
|
||||
if (_default) {
|
||||
const {_quickReplies} = _default;
|
||||
if (_quickReplies) return _quickReplies.text || _quickReplies.fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
const parseBotText = (evt) => {
|
||||
const {text, data} = evt;
|
||||
if (text !== undefined) return String(text);
|
||||
|
||||
switch (data?.type) {
|
||||
case 'quickReplies':
|
||||
return parseQuickReplies(data?._cognigy);
|
||||
case 'gallery':
|
||||
return parseGallery(data?._cognigy);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
class Cognigy extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.url = this.data.url;
|
||||
this.token = this.data.token;
|
||||
this.prompt = this.data.prompt;
|
||||
this.eventHook = this.data?.eventHook;
|
||||
this.actionHook = this.data?.actionHook;
|
||||
this.data = this.data.data || {};
|
||||
this.prompts = [];
|
||||
this.retry = {};
|
||||
this.timeoutCount = 0;
|
||||
// create a task queue so we can execute our taskss subsequently
|
||||
// also executing tasks whenever they come in
|
||||
|
||||
this.taskQueue = queue({concurrency: 1, autostart: 1});
|
||||
this.changeConfigTasks = [];
|
||||
|
||||
// keep track of turns so we only do gather once per turn
|
||||
this.turn = 0;
|
||||
this.gatherTurn = 0;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Cognigy; }
|
||||
|
||||
get hasReportedFinalAction() {
|
||||
return this.reportedFinalAction || this.isReplacingApplication;
|
||||
}
|
||||
|
||||
async _enqueueTask(task) {
|
||||
let resolver;
|
||||
let rejector;
|
||||
|
||||
const boundTask = task.bind(this);
|
||||
const taskPromise = new Promise(async(resolve, reject) => {
|
||||
resolver = resolve;
|
||||
rejector = reject;
|
||||
});
|
||||
taskPromise.resolve = resolver;
|
||||
this.taskQueue.push(async(cb) => {
|
||||
this.logger.debug('executing task from queue');
|
||||
try {
|
||||
|
||||
const result = await boundTask();
|
||||
// if this is a config task, remove it from the config task storage,
|
||||
// as we have now executed it
|
||||
if(task.isConfigTask){
|
||||
this.changeConfigTasks.shift();
|
||||
}
|
||||
resolver(result);
|
||||
cb(result);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'could not execute task in task queue');
|
||||
rejector(err);
|
||||
cb(err);
|
||||
}
|
||||
this.logger.debug('say task executed from queue');
|
||||
});
|
||||
|
||||
// if this is a config task, lets also push the config
|
||||
if(task.isConfigTask){
|
||||
this.changeConfigTasks.push(task);
|
||||
}
|
||||
if (this.taskQueue.lastPromise) {
|
||||
// resolve the previous promise for cleanup
|
||||
this.taskQueue.lastPromise.resolve({});
|
||||
}
|
||||
this.taskQueue.lastPromise = taskPromise;
|
||||
return taskPromise;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
const opts = {
|
||||
synthesizer: this.data.synthesizer || {
|
||||
vendor: 'default',
|
||||
language: 'default',
|
||||
voice: 'default'
|
||||
},
|
||||
recognizer: this.data.recognizer || {
|
||||
vendor: 'default',
|
||||
language: 'default'
|
||||
},
|
||||
bargein: this.data.bargein || {},
|
||||
bot: this.data.bot || {},
|
||||
user: this.data.user || {},
|
||||
dtmf: this.data.dtmf || {}
|
||||
};
|
||||
this.config = new SpeechConfig({logger: this.logger, ep, opts});
|
||||
this.ep = ep;
|
||||
try {
|
||||
|
||||
/* set event handlers and start transcribing */
|
||||
this.on('transcription', this._onTranscription.bind(this, cs, ep));
|
||||
this.on('dtmf-collected', this._onDtmf.bind(this, cs, ep));
|
||||
this.on('timeout', this._onTimeout.bind(this, cs, ep));
|
||||
this.on('error', this._onError.bind(this, cs, ep));
|
||||
|
||||
/* connect to the bot and send initial data */
|
||||
this.client = new SocketClient(
|
||||
this.url,
|
||||
this.token,
|
||||
{
|
||||
sessionId: cs.callSid,
|
||||
channel: 'jambonz',
|
||||
forceWebsockets: true,
|
||||
reconnection: true,
|
||||
settings: {
|
||||
enableTypingIndicator: false
|
||||
}
|
||||
}
|
||||
);
|
||||
this.client.on('output', this._onBotUtterance.bind(this, cs, ep));
|
||||
this.client.on('error', this._onBotError.bind(this, cs, ep));
|
||||
this.client.on('finalPing', this._onBotFinalPing.bind(this, cs, ep));
|
||||
await this.client.connect();
|
||||
// todo make welcome message configurable (enable or disable it when
|
||||
// we start a conversation (should be enabled by defaul))
|
||||
this.client.sendMessage('Welcome Message', {...this.data, ...cs.callInfo});
|
||||
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Cognigy error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.debug('Cognigy:kill');
|
||||
|
||||
|
||||
this.removeAllListeners();
|
||||
this.transcribeTask && this.transcribeTask.kill();
|
||||
|
||||
this.client.removeAllListeners();
|
||||
if (this.client && this.client.connected) this.client.disconnect();
|
||||
|
||||
try {
|
||||
// end the task queue AFTER we have removed all listeneres since now we cannot get new stuff inside the queue
|
||||
this.taskQueue.end();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'could not end tasks queue!!');
|
||||
}
|
||||
|
||||
|
||||
if (!this.hasReportedFinalAction) {
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'caller hungup'})
|
||||
.catch((err) => this.logger.info({err}, 'cognigy - error w/ action webook'));
|
||||
}
|
||||
|
||||
if (this.ep.connected) {
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promt which will be sent to the consumer. We will create a say task if bargein is disabled
|
||||
* for session and nextTurn, else create a gather task.
|
||||
*/
|
||||
_createPromtTask({text, url, turnConfig, listenAfterSpeech} = {}) {
|
||||
const bargeInOnNextTurn = turnConfig?.bargein?.enable?.length > 0;
|
||||
const bargeInSession = this.config.bargeInEnabled;
|
||||
if (bargeInOnNextTurn || bargeInSession) {
|
||||
return this._makeGatherTask({textPrompt: text, url, turnConfig, listenAfterSpeech});
|
||||
}
|
||||
return this._makeSayTask({text, turnConfig});
|
||||
}
|
||||
|
||||
_makeGatherTask({textPrompt, urlPrompt, turnConfig, listenAfterSpeech} = {}) {
|
||||
this.logger.debug({textPrompt, urlPrompt, turnConfig}, '_makeGatherTask');
|
||||
const config = this.config.makeGatherTaskConfig({textPrompt, urlPrompt, turnConfig, listenAfterSpeech});
|
||||
const {retry, ...rest} = config;
|
||||
this.retry = retry;
|
||||
const gather = makeTask(this.logger, {gather: rest}, this);
|
||||
return gather;
|
||||
}
|
||||
|
||||
_makeSayTask({ text, turnConfig } = {}) {
|
||||
this.logger.debug({text, turnConfig}, '_makeSayTask');
|
||||
const config = this.config.makeSayTaskConfig({text, turnConfig });
|
||||
this.logger.debug({config}, 'created say task config');
|
||||
const say = makeTask(this.logger, { say: config }, this);
|
||||
return say;
|
||||
}
|
||||
|
||||
_makeReferTask(referTo) {
|
||||
return makeTask(this.logger, {'sip:refer': {
|
||||
referTo
|
||||
}}
|
||||
);
|
||||
}
|
||||
|
||||
_makeHangupTask(reason) {
|
||||
return makeTask(this.logger, {
|
||||
hangup: {
|
||||
headers: {
|
||||
'X-Reason': reason
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
_makePlayTask(url, loop) {
|
||||
return makeTask(this.logger, {
|
||||
play: {
|
||||
url,
|
||||
loop
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* if we need to interrupt the currently-running say task(s), call this */
|
||||
_killSayTasks(ep) {
|
||||
if (ep && ep.connected) {
|
||||
ep.api('uuid_break', this.ep.uuid)
|
||||
.catch((err) => this.logger.info({err}, 'Cognigy:_killSayTasks - error killing audio for current say task'));
|
||||
}
|
||||
}
|
||||
|
||||
async _onBotError(cs, ep, evt) {
|
||||
this.logger.info({evt}, 'Cognigy:_onBotError');
|
||||
this.performAction({cognigyResult: 'botError', message: evt.message });
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onBotFinalPing(cs, ep) {
|
||||
this.logger.info({prompts: this.prompts}, 'Cognigy:_onBotFinalPing');
|
||||
try {
|
||||
// lets wait until we have finished processing the speech before
|
||||
// starting a gather...
|
||||
this.logger.debug('enqueued bot final ping gather');
|
||||
this._enqueueTask(async() => {
|
||||
this.logger.debug('executing bot final ping gather');
|
||||
try {
|
||||
const gatherTask = this._makeGatherTask();
|
||||
await gatherTask.exec(cs, ep, this);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Cognigy final ping gather task returned error');
|
||||
}
|
||||
this.logger.debug('executed bot final ping gather');
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Cognigy gather task returned error');
|
||||
}
|
||||
}
|
||||
|
||||
async _onBotUtterance(cs, ep, evt) {
|
||||
this.logger.debug({evt}, 'Cognigy:_onBotUtterance');
|
||||
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'botMessage', message: evt})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Cognigy_onBotUtterance: event handler for bot message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'redirect'}, false);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Cognigy_onBotUtterance: error sending event hook');
|
||||
});
|
||||
}
|
||||
|
||||
const text = parseBotText(evt);
|
||||
|
||||
// only add say task if its a normal cognigy node and not a "gather task"
|
||||
if (text && (evt?.data?.type !== 'promt')) {
|
||||
this.logger.info({text}, 'received text');
|
||||
this._enqueueTask(async() => {
|
||||
// todo inject the session config into the say task
|
||||
const promtTask = this._createPromtTask({ text, listenAfterSpeech: false });
|
||||
await promtTask.exec(cs, ep, this);
|
||||
this.logger.debug({text}, 'executed say task');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
switch (evt?.data?.type) {
|
||||
case 'hangup':
|
||||
this._enqueueTask(async() => {
|
||||
this.performAction({cognigyResult: 'hangup Succeeded'});
|
||||
this.reportedFinalAction = true;
|
||||
cs.replaceApplication([this._makeHangupTask(evt.data.reason)]);
|
||||
this.taskQueue.end();
|
||||
});
|
||||
return;
|
||||
case 'refer':
|
||||
this._enqueueTask(async() => {
|
||||
this.performAction({cognigyResult: 'refer succeeded'});
|
||||
this.reportedFinalAction = true;
|
||||
cs.replaceApplication([this._makeReferTask(evt.data.referTo)]);
|
||||
});
|
||||
return;
|
||||
case 'promt':
|
||||
this._enqueueTask(async() => {
|
||||
const sayTask = this._createPromtTask({
|
||||
text: evt.data.text,
|
||||
turnConfig: evt?.data?.config?.nextTurn,
|
||||
url: evt.data.url
|
||||
});
|
||||
try {
|
||||
await sayTask.exec(cs, ep, this);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Cognigy sayTask task returned error');
|
||||
}
|
||||
});
|
||||
return;
|
||||
case 'setSessionConfig':
|
||||
// change session params in the order they come in with the say tasks
|
||||
// so we are consistent with the flow logic executed within cognigy
|
||||
const updateConfigTask = () => {
|
||||
if (evt?.data?.config?.session) this.config.update(evt.data.config.session);
|
||||
};
|
||||
updateConfigTask.isConfigTask = true;
|
||||
this._enqueueTask(updateConfigTask);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err, evtData: evt.data}, 'encountered error parsing cognigy response data');
|
||||
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
async _onTranscription(cs, ep, evt) {
|
||||
this.logger.debug({evt}, `Cognigy: got transcription for callSid ${cs.callSid}`);
|
||||
const utterance = evt.alternatives[0].transcript;
|
||||
|
||||
//if we have barge in enabled AND we enabled skipping until next question
|
||||
//then stop execution of currently queues bot output before sending the
|
||||
//response to waiting bot since otherwise we could stop upcoming bot output
|
||||
|
||||
if (this.config.bargeInEnabled && this.config.skipToBotOutputEnd !== false) {
|
||||
// clear task queue, resolve the last promise and cleanup;
|
||||
this._killSayTasks();
|
||||
this.taskQueue.lastPromise.resolve();
|
||||
this.taskQueue.end();
|
||||
while(this.changeConfigTasks.length > 0){
|
||||
// apply all the config tasks FIFO
|
||||
const changeConfigTask = this.changeConfigTasks.shift();
|
||||
changeConfigTask();
|
||||
}
|
||||
this.taskQueue.autostart = true;
|
||||
}
|
||||
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'userMessage', message: utterance})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Cognigy_onTranscription: event handler for user message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'redirect'}, false);
|
||||
if (this.transcribeTask) this.transcribeTask.kill(cs);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Cognigy_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
|
||||
/* send the user utterance to the bot */
|
||||
try {
|
||||
if (this.client && this.client.connected) {
|
||||
this.client.sendMessage(utterance);
|
||||
}
|
||||
else {
|
||||
// if the bot is not connected, should we maybe throw an error here?
|
||||
this.logger.info('Cognigy_onTranscription - not sending user utterance as bot is disconnected');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Cognigy_onTranscription: Error sending user utterance to Cognigy - ending task');
|
||||
this.performAction({cognigyResult: 'socketError'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
this.logger.info({evt}, 'got dtmf');
|
||||
|
||||
/* send dtmf to bot */
|
||||
try {
|
||||
if (this.client && this.client.connected) {
|
||||
this.client.sendMessage(String(evt.digits));
|
||||
}
|
||||
else {
|
||||
// if the bot is not connected, should we maybe throw an error here?
|
||||
this.logger.info('Cognigy_onTranscription - not sending user dtmf as bot is disconnected');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, '_onDtmf: Error sending user dtmf to Cognigy - ending task');
|
||||
this.performAction({cognigyResult: 'socketError'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
_onError(cs, ep, err) {
|
||||
this.logger.info({err}, 'Cognigy: got error');
|
||||
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onTimeout(cs, ep, evt) {
|
||||
const {noInputRetries, noInputSpeech, noInputUrl} = this.retry;
|
||||
this.logger.debug({evt, retry: this.retry}, 'Cognigy: got timeout');
|
||||
if (noInputRetries && this.timeoutCount++ < noInputRetries) {
|
||||
const gatherTask = this._makeGatherTask({textPrompt: noInputSpeech, urlPrompt: noInputUrl});
|
||||
gatherTask.exec(cs, ep, this)
|
||||
.catch((err) => this.logger.info({err}, 'Cognigy gather task returned error'));
|
||||
}
|
||||
else {
|
||||
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'timeout'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cognigy;
|
||||
148
lib/tasks/cognigy/speech-config.js
Normal file
148
lib/tasks/cognigy/speech-config.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const obj = require('drachtio-fsmrf/lib/utils');
|
||||
const Emitter = require('events');
|
||||
const { isArray } = require('lodash');
|
||||
const lodash = require('lodash');
|
||||
const hasKeys = (obj) => typeof obj === 'object' && Object.keys(obj) > 0;
|
||||
|
||||
const stripNulls = (obj) => {
|
||||
Object.keys(obj).forEach((k) => (obj[k] === null || typeof obj[k] === 'undefined') && delete obj[k]);
|
||||
return obj;
|
||||
};
|
||||
|
||||
class SpeechConfig extends Emitter {
|
||||
constructor({logger, ep, opts = {}}) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.ep = ep;
|
||||
this.sessionConfig = opts.session || {};
|
||||
this.update(opts);
|
||||
}
|
||||
|
||||
_mergeConfig(changedConfig = {}) {
|
||||
const merged = lodash.mergeWith(
|
||||
this.sessionConfig,
|
||||
changedConfig,
|
||||
(objValue, sourceValue) => {
|
||||
if (Array.isArray(objValue)) {
|
||||
if (Array.isArray(sourceValue)) {
|
||||
return sourceValue;
|
||||
}
|
||||
return objValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
this.logger.debug({merged, sessionConfig: this.sessionConfig, changedConfig}, 'merged config');
|
||||
// should we override hints with empty array or leave it as it is once saved?
|
||||
// merged.recognizer.hints = changedConfig.recognizer?.hints
|
||||
return merged;
|
||||
}
|
||||
|
||||
update(session) {
|
||||
// TODO validation of session params?
|
||||
if (session) {
|
||||
this.sessionConfig = this._mergeConfig(session);
|
||||
}
|
||||
this.logger.debug({sessionLevel: this.sessionConfig}, 'SpeechConfig updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* check if we should skip all nodes until next bot input
|
||||
*/
|
||||
get skipUntilBotInput() {
|
||||
return !this.sessionConfig.bargein?.skipUntilBotInput;
|
||||
}
|
||||
/**
|
||||
* Check if barge is enabled on session level
|
||||
*/
|
||||
get bargeInEnabled() {
|
||||
return this.sessionConfig.bargein?.enable?.length > 0;
|
||||
}
|
||||
|
||||
makeSayTaskConfig({text, turnConfig = {}} = {}) {
|
||||
const synthesizer = lodash.merge({}, this.sessionConfig.synthesizer, turnConfig.synthesizer);
|
||||
return {
|
||||
text,
|
||||
synthesizer
|
||||
};
|
||||
}
|
||||
|
||||
makeGatherTaskConfig({textPrompt, urlPrompt, turnConfig = {}, listenAfterSpeech} = {}) {
|
||||
// we merge from top to bottom deeply so we wil have
|
||||
// defaults from session config and then will override them via turn config
|
||||
const opts = this._mergeConfig(turnConfig);
|
||||
|
||||
this.logger.debug({
|
||||
opts,
|
||||
sessionConfig: this.sessionConfig,
|
||||
turnConfig
|
||||
}, 'Congigy SpeechConfig:_makeGatherTask current options');
|
||||
|
||||
/* input type: speech and/or dtmf entry */
|
||||
const input = [];
|
||||
if (opts.recognizer) input.push('speech');
|
||||
if (hasKeys(opts.dtmf)) input.push('digits');
|
||||
|
||||
if (opts.synthesizer) {
|
||||
// todo remove this once we add support for disabling tts cache
|
||||
delete opts.synthesizer.disableTtsCache;
|
||||
}
|
||||
|
||||
/* bargein settings */
|
||||
const bargein = opts.bargein || {};
|
||||
const speechBargein = Array.isArray(bargein.enable) && bargein.enable.includes('speech');
|
||||
const dtmfBargein = Array.isArray(bargein.enable) && bargein.enable.includes('dtmf');
|
||||
const minBargeinWordCount = speechBargein ? (bargein.minWordCount || 1) : 0;
|
||||
const {interDigitTimeout = 0, maxDigits, minDigits = 1, submitDigit} = (opts.dtmf || {});
|
||||
const {noInputTimeout, noInputRetries, noInputSpeech, noInputUrl} = (opts.user || {});
|
||||
|
||||
let sayConfig;
|
||||
let playConfig;
|
||||
|
||||
if (textPrompt) {
|
||||
sayConfig = {
|
||||
text: textPrompt,
|
||||
synthesizer: opts.synthesizer
|
||||
};
|
||||
}
|
||||
|
||||
// todo what is the logic here if we put both? play over say or say over play?
|
||||
if (urlPrompt) {
|
||||
playConfig = {
|
||||
url: urlPrompt
|
||||
};
|
||||
}
|
||||
|
||||
const config = {
|
||||
input,
|
||||
listenDuringPrompt: speechBargein,
|
||||
bargein: speechBargein,
|
||||
minBargeinWordCount,
|
||||
dtmfBargein,
|
||||
minDigits,
|
||||
maxDigits,
|
||||
interDigitTimeout,
|
||||
finishOnKey: submitDigit,
|
||||
recognizer: opts?.recognizer,
|
||||
timeout: noInputTimeout,
|
||||
retry : {
|
||||
noInputRetries,
|
||||
noInputSpeech,
|
||||
noInputUrl
|
||||
},
|
||||
listenAfterSpeech
|
||||
};
|
||||
|
||||
const final = stripNulls(config);
|
||||
|
||||
const finalConfig = final;
|
||||
if (sayConfig) {
|
||||
finalConfig.say = sayConfig;
|
||||
} else if (playConfig) {
|
||||
finalConfig.play = playConfig;
|
||||
}
|
||||
this.logger.info({finalConfig}, 'created gather config');
|
||||
return finalConfig;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SpeechConfig;
|
||||
@@ -453,7 +453,7 @@ class Conference extends Task {
|
||||
this._playSession = null;
|
||||
break;
|
||||
}
|
||||
} while (!this.killed && this.conf_hold_status !== 'hold');
|
||||
} while (!this.killed && !this.conf_hold_status === 'hold');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,7 +529,7 @@ class Conference extends Task {
|
||||
|
||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
|
||||
assert(!this._playSession);
|
||||
const json = await cs.application.requestor.request('verb:hook', hook, cs.callInfo);
|
||||
const json = await cs.application.requestor.request(hook, cs.callInfo);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
@@ -586,7 +586,7 @@ class Conference extends Task {
|
||||
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))
|
||||
cs.application.requestor.request(this.statusHook, Object.assign(params, this.statusParams))
|
||||
.catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
|
||||
class TaskConfig extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'synthesizer',
|
||||
'recognizer',
|
||||
'bargeIn'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if (this.hasBargeIn && this.bargeIn.enable === true) {
|
||||
this.gatherOpts = {
|
||||
verb: 'gather',
|
||||
timeout: 0
|
||||
};
|
||||
[
|
||||
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'dtmfBargein', 'actionHook'
|
||||
].forEach((k) => {
|
||||
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
|
||||
});
|
||||
|
||||
this.preconditions = this.hasBargeIn ? TaskPreconditions.Endpoint : TaskPreconditions.None;
|
||||
}
|
||||
}
|
||||
|
||||
get name() { return TaskName.Config; }
|
||||
|
||||
get hasSynthesizer() { return Object.keys(this.synthesizer).length; }
|
||||
|
||||
get hasRecognizer() { return Object.keys(this.recognizer).length; }
|
||||
|
||||
get hasBargeIn() { return Object.keys(this.bargeIn).length; }
|
||||
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
|
||||
if (this.hasSynthesizer) {
|
||||
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
|
||||
? this.synthesizer.vendor
|
||||
: cs.speechSynthesisVendor;
|
||||
cs.speechSynthesisLanguage = this.synthesizer.language !== 'default'
|
||||
? this.synthesizer.language
|
||||
: cs.speechSynthesisLanguage;
|
||||
cs.speechSynthesisVoice = this.synthesizer.voice !== 'default'
|
||||
? this.synthesizer.voice
|
||||
: cs.speechSynthesisVoice;
|
||||
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
|
||||
}
|
||||
if (this.hasRecognizer) {
|
||||
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
|
||||
? this.recognizer.vendor
|
||||
: cs.speechRecognizerVendor;
|
||||
cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
|
||||
? this.recognizer.language
|
||||
: cs.speechRecognizerLanguage;
|
||||
this.logger.info({recognizer: this.recognizer}, 'Config: updated recognizer');
|
||||
}
|
||||
if (this.hasBargeIn) {
|
||||
if (this.gatherOpts) {
|
||||
this.logger.debug({opts: this.gatherOpts}, 'Config: enabling bargeIn');
|
||||
cs.enableBotMode(this.gatherOpts);
|
||||
}
|
||||
else {
|
||||
this.logger.debug('Config: disabling bargeIn');
|
||||
cs.disableBotMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskConfig;
|
||||
@@ -14,7 +14,6 @@ const sessionTracker = require('../session/session-tracker');
|
||||
const DtmfCollector = require('../utils/dtmf-collector');
|
||||
const dbUtils = require('../utils/db-utils');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
|
||||
function parseDtmfOptions(logger, dtmfCapture) {
|
||||
let parentDtmfCollector, childDtmfCollector;
|
||||
@@ -92,7 +91,6 @@ class TaskDial extends Task {
|
||||
this.timeLimit = this.data.timeLimit;
|
||||
this.confirmHook = this.data.confirmHook;
|
||||
this.confirmMethod = this.data.confirmMethod;
|
||||
this.referHook = this.data.referHook;
|
||||
this.dtmfHook = this.data.dtmfHook;
|
||||
this.proxy = this.data.proxy;
|
||||
|
||||
@@ -137,11 +135,6 @@ class TaskDial extends Task {
|
||||
return !process.env.ANCHOR_MEDIA_ALWAYS && !this.listenTask && !this.transcribeTask;
|
||||
}
|
||||
|
||||
get summary() {
|
||||
if (this.target.length === 1) return `${this.name}{type=${this.target[0].type}}`;
|
||||
else return `${this.name}{${this.target.length} targets}`;
|
||||
}
|
||||
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
try {
|
||||
@@ -154,7 +147,7 @@ class TaskDial extends Task {
|
||||
this.epOther.play(this.dialMusic).catch((err) => {});
|
||||
}
|
||||
}
|
||||
if (!this.killed) await this._attemptCalls(cs);
|
||||
await this._attemptCalls(cs);
|
||||
await this.awaitTaskDone();
|
||||
this.logger.debug({callSid: this.cs.callSid}, 'Dial:exec task is done, sending actionHook if any');
|
||||
await this.performAction(this.results, this.killReason !== KillReason.Replaced);
|
||||
@@ -247,40 +240,6 @@ class TaskDial extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
async handleRefer(cs, req, res, callInfo = cs.callInfo) {
|
||||
if (this.referHook) {
|
||||
try {
|
||||
const isChild = !!callInfo.parentCallSid;
|
||||
const referring_call_sid = isChild ? callInfo.callSid : cs.callSid;
|
||||
const referred_call_sid = isChild ? callInfo.parentCallSid : this.sd.callSid;
|
||||
|
||||
const to = parseUri(req.getParsedHeader('Refer-To').uri);
|
||||
const by = parseUri(req.getParsedHeader('Referred-By').uri);
|
||||
this.logger.info({to}, 'refer to parsed');
|
||||
await cs.requestor.request('verb:hook', this.referHook, {
|
||||
...callInfo,
|
||||
refer_details: {
|
||||
sip_refer_to: req.get('Refer-To'),
|
||||
sip_referred_by: req.get('Referred-By'),
|
||||
sip_user_agent: req.get('User-Agent'),
|
||||
refer_to_user: to.user,
|
||||
referred_by_user: by.user,
|
||||
referring_call_sid,
|
||||
referred_call_sid
|
||||
}
|
||||
});
|
||||
res.send(202);
|
||||
this.logger.info('DialTask:handleRefer - sent 202 Accepted');
|
||||
} catch (err) {
|
||||
res.send(err.statusCode || 501);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.logger.info('DialTask:handleRefer - got REFER but no referHook, responding 501');
|
||||
res.send(501);
|
||||
}
|
||||
}
|
||||
|
||||
_removeHandlers(sd) {
|
||||
sd.removeAllListeners('accept');
|
||||
sd.removeAllListeners('decline');
|
||||
@@ -329,7 +288,7 @@ class TaskDial extends Task {
|
||||
const match = dtmfDetector.keyPress(key);
|
||||
if (match) {
|
||||
this.logger.info({callSid}, `Dial:_onInfo triggered dtmf match: ${match}`);
|
||||
requestor.request('verb:hook', this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
|
||||
requestor.request(this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
@@ -408,9 +367,6 @@ class TaskDial extends Task {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.killed) return;
|
||||
|
||||
const sd = placeCall({
|
||||
logger: this.logger,
|
||||
application: cs.application,
|
||||
@@ -425,11 +381,8 @@ class TaskDial extends Task {
|
||||
this.dials.set(sd.callSid, sd);
|
||||
|
||||
sd
|
||||
.on('refer', (callInfo, req, res) => this.handleRefer(cs, req, res, callInfo))
|
||||
.on('callCreateFail', () => {
|
||||
clearTimeout(this.timerRing);
|
||||
this.dials.delete(sd.callSid);
|
||||
sd.removeAllListeners();
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after call create err, ending task');
|
||||
this.kill(cs);
|
||||
@@ -471,7 +424,6 @@ class TaskDial extends Task {
|
||||
})
|
||||
.on('accept', async() => {
|
||||
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
||||
clearTimeout(this.timerRing);
|
||||
try {
|
||||
await this._connectSingleDial(cs, sd);
|
||||
} catch (err) {
|
||||
@@ -480,9 +432,7 @@ class TaskDial extends Task {
|
||||
})
|
||||
.on('decline', () => {
|
||||
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
|
||||
clearTimeout(this.timerRing);
|
||||
this.dials.delete(sd.callSid);
|
||||
sd.removeAllListeners();
|
||||
if (this.dials.size === 0 && !this.sd) {
|
||||
this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending task');
|
||||
this.kill(cs);
|
||||
@@ -494,9 +444,6 @@ class TaskDial extends Task {
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error in dial einvite from B leg');
|
||||
}
|
||||
})
|
||||
.on('refer', (callInfo, req, res) => {
|
||||
|
||||
})
|
||||
.once('adulting', () => {
|
||||
/* child call just adulted and got its own session */
|
||||
|
||||
@@ -453,7 +453,7 @@ class Dialogflow extends Task {
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results = {}) {
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, {...results, ...cs.callInfo.toJSON()});
|
||||
const json = await this.cs.requestor.request(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));
|
||||
|
||||
@@ -317,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);
|
||||
const json = await cs.application.requestor.request(hook, params);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
|
||||
@@ -9,7 +9,6 @@ const {
|
||||
|
||||
const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7);
|
||||
|
||||
class TaskGather extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
@@ -18,31 +17,23 @@ class TaskGather extends Task {
|
||||
|
||||
[
|
||||
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
'interDigitTimeout', 'submitDigit', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
'retries', 'retryPromptTts', 'retryPromptUrl',
|
||||
'speechTimeout', 'timeout', 'say', 'play'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
|
||||
/* when collecting dtmf, bargein on dtmf is true unless explicitly set to false */
|
||||
if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true;
|
||||
|
||||
/* timeout of zero means no timeout */
|
||||
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
|
||||
this.interim = this.partialResultHook || this.bargein;
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
|
||||
|
||||
this.logger.debug({opts}, 'created gather task');
|
||||
this.timeout = (this.timeout || 15) * 1000;
|
||||
this.interim = this.partialResultCallback || this.bargein;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
this.hints = recognizer.hints || [];
|
||||
this.hintsBoost = recognizer.hintsBoost;
|
||||
this.altLanguages = recognizer.altLanguages || [];
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 0;
|
||||
|
||||
/* aws options */
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
@@ -53,7 +44,10 @@ class TaskGather extends Task {
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
}
|
||||
|
||||
this.digitBuffer = '';
|
||||
@@ -61,7 +55,12 @@ class TaskGather extends Task {
|
||||
|
||||
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;
|
||||
|
||||
if(this.sayTask || this.playTask){
|
||||
// this is specially for barge in where we want to make a bargebale promt
|
||||
// to a user without listening after the say task has finished
|
||||
this.listenAfterSpeech = typeof this.data.listenAfterSpeech === "boolean" ? this.data.listenAfterSpeech : true;
|
||||
}
|
||||
|
||||
this.parentTask = parentTask;
|
||||
}
|
||||
@@ -75,23 +74,7 @@ class TaskGather extends Task {
|
||||
(this.playTask && this.playTask.earlyMedia);
|
||||
}
|
||||
|
||||
get summary() {
|
||||
let s = `${this.name}{`;
|
||||
if (this.input.length === 2) s += 'inputs=[speech,digits],';
|
||||
else if (this.input.includes('digits')) s += 'inputs=digits';
|
||||
else s += 'inputs=speech,';
|
||||
|
||||
if (this.input.includes('speech')) {
|
||||
s += `vendor=${this.vendor || 'default'},language=${this.language || 'default'}`;
|
||||
}
|
||||
if (this.sayTask) s += ',with nested say task';
|
||||
if (this.playTask) s += ',with nested play task';
|
||||
s += '}';
|
||||
return s;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
this.logger.debug('Gather:exec');
|
||||
await super.exec(cs);
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
|
||||
@@ -114,6 +97,7 @@ class TaskGather extends Task {
|
||||
const startListening = (cs, ep) => {
|
||||
this._startTimer();
|
||||
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
||||
this.logger.debug('listening after speech 1');
|
||||
this._initSpeech(cs, ep)
|
||||
.then(() => {
|
||||
this._startTranscribing(ep);
|
||||
@@ -125,22 +109,40 @@ class TaskGather extends Task {
|
||||
|
||||
try {
|
||||
if (this.sayTask) {
|
||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.on('playDone', (err) => {
|
||||
this.logger.debug('Gather: kicking off say task');
|
||||
this.sayTask.exec(cs, ep);
|
||||
this.sayTask.on('playDone', async(err) => {
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.debug('Gather: say task completed');
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
this.logger.debug('listening after speech 2');
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this.playTask) {
|
||||
this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.on('playDone', (err) => {
|
||||
this.playTask.on('playDone', async(err) => {
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
if (!this.killed) startListening(cs, ep);
|
||||
});
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
this.logger.debug('listening after speech 3');
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else startListening(cs, ep);
|
||||
|
||||
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
||||
this.logger.debug('listening after speech 4');
|
||||
await this._initSpeech(cs, ep);
|
||||
this._startTranscribing(ep);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
|
||||
@@ -152,6 +154,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
await this.awaitTaskDone();
|
||||
this.logger.debug('Gather:exec task has completed');
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'TaskGather:exec error');
|
||||
}
|
||||
@@ -163,25 +166,19 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
kill(cs) {
|
||||
this.logger.debug('Gather:kill');
|
||||
super.kill(cs);
|
||||
this._killAudio(cs);
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
this._resolve('killed');
|
||||
}
|
||||
|
||||
updateTimeout(timeout) {
|
||||
this.logger.info(`TaskGather:updateTimout - updating timeout to ${timeout}`);
|
||||
this.timeout = timeout;
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
let resolved = false;
|
||||
if (this.dtmfBargein) this._killAudio(cs);
|
||||
if (evt.dtmf === this.finishOnKey && this.input.includes('digits')) {
|
||||
if (evt.dtmf === this.finishOnKey) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-terminator-key');
|
||||
}
|
||||
@@ -193,6 +190,7 @@ class TaskGather extends Task {
|
||||
this._resolve('dtmf-num-digits');
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved && this.interDigitTimeout > 0 && this.digitBuffer.length >= this.minDigits) {
|
||||
/* start interDigitTimer */
|
||||
const ms = this.interDigitTimeout * 1000;
|
||||
@@ -203,13 +201,11 @@ class TaskGather extends Task {
|
||||
|
||||
async _initSpeech(cs, ep) {
|
||||
const opts = {};
|
||||
|
||||
if (this.vad?.enable) {
|
||||
if (this.vad.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
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);
|
||||
Object.assign(opts, {
|
||||
@@ -219,11 +215,8 @@ class TaskGather extends Task {
|
||||
});
|
||||
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 && this.altLanguages.length > 0) {
|
||||
if (this.altLanguages && this.altLanguages.length > 1) {
|
||||
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
if (this.profanityFilter === true) {
|
||||
@@ -257,9 +250,8 @@ class TaskGather extends Task {
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
|
||||
//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;
|
||||
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
|
||||
@@ -271,7 +263,6 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
_startTranscribing(ep) {
|
||||
this.logger.debug('Gather:_startTranscribing');
|
||||
ep.startTranscription({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
@@ -289,7 +280,6 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
_startTimer() {
|
||||
if (0 === this.timeout) return;
|
||||
assert(!this._timeoutTimer);
|
||||
this.logger.debug(`Gather:_startTimer: timeout ${this.timeout}`);
|
||||
this._timeoutTimer = setTimeout(() => this._resolve('timeout'), this.timeout);
|
||||
@@ -316,6 +306,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const final = evt.RecognitionStatus === 'Success';
|
||||
@@ -344,29 +335,21 @@ class TaskGather extends Task {
|
||||
}
|
||||
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
|
||||
others do not.
|
||||
*/
|
||||
const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
|
||||
|
||||
if (this.bargein && isStableEnough &&
|
||||
if (evt.stability > 0.70 &&
|
||||
this.bargein &&
|
||||
evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) {
|
||||
this.logger.debug('Gather:_onTranscription - killing audio due to speech bargein');
|
||||
this.logger.debug('Gather:_onTranscription - killing audio due to bargein');
|
||||
this._killAudio(cs);
|
||||
this._resolve('speech', evt);
|
||||
}
|
||||
if (this.partialResultHook) {
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo));
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
_onEndOfUtterance(cs, ep) {
|
||||
this.logger.info('TaskGather:_onEndOfUtterance');
|
||||
if (this.bargein && this.minBargeinWordCount === 0) {
|
||||
this.logger.debug('Gather:_onTranscription - killing audio due to utterance detected');
|
||||
this._killAudio(cs);
|
||||
}
|
||||
|
||||
if (!this.resolved && !this.killed) {
|
||||
this._startTranscribing(ep);
|
||||
}
|
||||
@@ -380,7 +363,6 @@ class TaskGather extends Task {
|
||||
if (this.resolved) return;
|
||||
this.resolved = true;
|
||||
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
|
||||
clearTimeout(this.interDigitTimer);
|
||||
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.ep.stopTranscription({vendor: this.vendor})
|
||||
@@ -389,25 +371,16 @@ class TaskGather extends Task {
|
||||
|
||||
this._clearTimer();
|
||||
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'});
|
||||
}
|
||||
if (this.parentTask) this.parentTask.emit('dtmf-collected', {reason, digits: this.digitBuffer});
|
||||
else 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 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 await this.performAction({reason: 'timeout'});
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ class Lex extends Task {
|
||||
}
|
||||
|
||||
async _performHook(cs, hook, results) {
|
||||
const json = await this.cs.requestor.request('verb:hook', hook, results);
|
||||
const json = await this.cs.requestor.request(hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -20,9 +20,9 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.SipRefer:
|
||||
const TaskSipRefer = require('./sip_refer');
|
||||
return new TaskSipRefer(logger, data, parent);
|
||||
case TaskName.Config:
|
||||
const TaskConfig = require('./config');
|
||||
return new TaskConfig(logger, data, parent);
|
||||
case TaskName.Cognigy:
|
||||
const TaskCognigy = require('./cognigy');
|
||||
return new TaskCognigy(logger, data, parent);
|
||||
case TaskName.Conference:
|
||||
const TaskConference = require('./conference');
|
||||
return new TaskConference(logger, data, parent);
|
||||
|
||||
@@ -48,7 +48,7 @@ class TaskRestDial extends Task {
|
||||
cs.setDialog(dlg);
|
||||
|
||||
try {
|
||||
const tasks = await cs.requestor.request('verb:hook', this.call_hook, cs.callInfo);
|
||||
const tasks = await cs.requestor.request(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)));
|
||||
|
||||
@@ -14,14 +14,6 @@ class TaskSay extends Task {
|
||||
|
||||
get name() { return TaskName.Say; }
|
||||
|
||||
get summary() {
|
||||
for (let i = 0; i < this.text.length; i++) {
|
||||
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.text[0];
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
@@ -29,20 +21,20 @@ class TaskSay extends Task {
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
|
||||
this.synthesizer.vendor :
|
||||
cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
|
||||
this.synthesizer.language :
|
||||
cs.speechSynthesisLanguage ;
|
||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||
this.synthesizer.voice :
|
||||
cs.speechSynthesisVoice;
|
||||
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ? this.synthesizer.vendor : cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ? this.synthesizer.language : cs.speechSynthesisLanguage ;
|
||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ? this.synthesizer.voice : cs.speechSynthesisVoice;
|
||||
const engine = this.synthesizer.engine || 'standard';
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
this.logger.info({vendor, language, voice}, 'TaskSay:exec');
|
||||
this.logger.info({language,
|
||||
voice,
|
||||
localSynthesizer: this.synthesizer,
|
||||
speechSynthesisVendor: cs.speechSynthesisVendor,
|
||||
speechSynthesisLanguage: cs.speechSynthesisLanguage,
|
||||
speechSynthesisVoice: cs.speechSynthesisVoice
|
||||
}, `Task:say - using vendor: ${vendor}`);
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
@@ -56,8 +48,6 @@ class TaskSay extends Task {
|
||||
// synthesize all of the text elements
|
||||
let lastUpdated = false;
|
||||
const filepath = (await Promise.all(this.text.map(async(text) => {
|
||||
if (this.killed) return;
|
||||
if (text.startsWith('silence_stream://')) return text;
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
text,
|
||||
vendor,
|
||||
@@ -87,7 +77,7 @@ class TaskSay extends Task {
|
||||
|
||||
this.logger.debug({filepath}, 'synthesized files for tts');
|
||||
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
||||
let segment = 0;
|
||||
do {
|
||||
if (cs.isInConference) {
|
||||
|
||||
@@ -19,11 +19,7 @@ class TaskSipDecline extends Task {
|
||||
res.send(this.data.status, this.data.reason, {
|
||||
headers: this.headers
|
||||
});
|
||||
cs.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Failed,
|
||||
sipStatus: this.data.status,
|
||||
sipReason: this.data.reason
|
||||
});
|
||||
cs.emit('callStatusChange', {callStatus: CallStatus.Failed, sipStatus: this.data.status});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ class TaskSipRefer extends Task {
|
||||
super.kill(cs);
|
||||
const {dlg} = cs;
|
||||
dlg.off('notify', this.notifyHandler);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _handleNotify(cs, dlg, req, res) {
|
||||
@@ -66,7 +65,7 @@ class TaskSipRefer extends Task {
|
||||
const status = arr[1];
|
||||
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
||||
if (this.eventHook) {
|
||||
await cs.requestor.request('verb:hook', this.eventHook, {event: 'transfer-status', call_status: status});
|
||||
await cs.requestor.request(this.eventHook, {event: 'transfer-status', call_status: status});
|
||||
}
|
||||
if (status >= 200) {
|
||||
await this.performAction({refer_status: 202, final_referred_call_status: status});
|
||||
|
||||
@@ -21,29 +21,20 @@
|
||||
"referTo"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"cognigy": {
|
||||
"properties": {
|
||||
"synthesizer": "#synthesizer",
|
||||
"url": "string",
|
||||
"token": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"bargeIn": "#bargeIn"
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"bargeIn": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"tts": "#synthesizer",
|
||||
"prompt": "string",
|
||||
"actionHook": "object|string",
|
||||
"input": "array",
|
||||
"finishOnKey": "string",
|
||||
"numDigits": "number",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"dtmfBargein": "boolean",
|
||||
"minBargeinWordCount": "number"
|
||||
"eventHook": "object|string",
|
||||
"data": "object"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
"url",
|
||||
"token"
|
||||
]
|
||||
},
|
||||
"dequeue": {
|
||||
@@ -107,16 +98,17 @@
|
||||
"finishOnKey": "string",
|
||||
"input": "array",
|
||||
"numDigits": "number",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"partialResultHook": "object|string",
|
||||
"speechTimeout": "number",
|
||||
"listenDuringPrompt": "boolean",
|
||||
"dtmfBargein": "boolean",
|
||||
"bargein": "boolean",
|
||||
"minBargeinWordCount": "number",
|
||||
"dtmfBargein": "boolean",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"timeout": "number",
|
||||
"listenAfterSpeech": "boolean",
|
||||
"recognizer": "#recognizer",
|
||||
"play": "#play",
|
||||
"say": "#say"
|
||||
@@ -149,7 +141,6 @@
|
||||
"answerOnBridge": "boolean",
|
||||
"callerId": "string",
|
||||
"confirmHook": "object|string",
|
||||
"referHook": "object|string",
|
||||
"dialMusic": "string",
|
||||
"dtmfCapture": "object",
|
||||
"dtmfHook": "object|string",
|
||||
@@ -355,7 +346,6 @@
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"]
|
||||
},
|
||||
"headers": "object",
|
||||
"name": "string",
|
||||
"number": "string",
|
||||
"sipUri": "string",
|
||||
@@ -407,9 +397,7 @@
|
||||
"enum": ["google", "aws", "microsoft", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"vad": "#vad",
|
||||
"hints": "array",
|
||||
"hintsBoost": "number",
|
||||
"altLanguages": "array",
|
||||
"profanityFilter": "boolean",
|
||||
"interim": "boolean",
|
||||
@@ -463,8 +451,7 @@
|
||||
]
|
||||
},
|
||||
"requestSnr": "boolean",
|
||||
"initialSpeechTimeoutMs": "number",
|
||||
"azureServiceEndpoint": "string"
|
||||
"initialSpeechTimeoutMs": "number"
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
@@ -478,15 +465,5 @@
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"vad": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"voiceMs": "number",
|
||||
"mode": "number"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,6 @@ class Task extends Emitter {
|
||||
return this.cs;
|
||||
}
|
||||
|
||||
get summary() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.data;
|
||||
}
|
||||
@@ -111,7 +107,7 @@ class Task extends Emitter {
|
||||
async performAction(results, expectResponse = true) {
|
||||
if (this.actionHook) {
|
||||
const params = results ? Object.assign(results, this.cs.callInfo.toJSON()) : this.cs.callInfo.toJSON();
|
||||
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params);
|
||||
const json = await this.cs.requestor.request(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));
|
||||
@@ -124,7 +120,7 @@ class Task extends Emitter {
|
||||
}
|
||||
|
||||
async performHook(cs, hook, results) {
|
||||
const json = await cs.requestor.request('verb:hook', hook, results);
|
||||
const json = await cs.requestor.request(hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
|
||||
@@ -22,13 +22,8 @@ class TaskTranscribe extends Task {
|
||||
this.interim = !!recognizer.interim;
|
||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
|
||||
/* google-specific options */
|
||||
this.hints = recognizer.hints || [];
|
||||
this.hintsBoost = recognizer.hintsBoost;
|
||||
this.profanityFilter = recognizer.profanityFilter;
|
||||
this.punctuation = !!recognizer.punctuation;
|
||||
this.enhancedModel = !!recognizer.enhancedModel;
|
||||
@@ -51,7 +46,6 @@ class TaskTranscribe extends Task {
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
@@ -111,12 +105,6 @@ class TaskTranscribe extends Task {
|
||||
async _startTranscribing(cs, ep) {
|
||||
const opts = {};
|
||||
|
||||
if (this.vad.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
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));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||
@@ -140,12 +128,7 @@ class TaskTranscribe extends Task {
|
||||
].forEach((arr) => {
|
||||
if (this[arr[0]]) opts[arr[1]] = true;
|
||||
});
|
||||
if (this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (typeof this.hintsBoost === 'number') {
|
||||
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
|
||||
}
|
||||
}
|
||||
if (this.hints.length > 1) opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
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;
|
||||
@@ -212,7 +195,6 @@ class TaskTranscribe extends Task {
|
||||
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
if (this.outputFormat !== 'simple') opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
|
||||
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with azure'));
|
||||
@@ -254,7 +236,7 @@ class TaskTranscribe extends Task {
|
||||
}
|
||||
|
||||
if (this.transcriptionHook) {
|
||||
this.cs.requestor.request('verb:hook', this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
|
||||
}
|
||||
if (this.parentTask) {
|
||||
|
||||
@@ -45,7 +45,6 @@ class SnsNotifier extends Emitter {
|
||||
}, 'response from SNS SubscribeURL');
|
||||
const data = await this.describeInstance();
|
||||
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState;
|
||||
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
||||
break;
|
||||
|
||||
case 'Notification':
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const Emitter = require('events');
|
||||
const crypto = require('crypto');
|
||||
const timeSeries = require('@jambonz/time-series');
|
||||
let alerter ;
|
||||
|
||||
class BaseRequestor extends Emitter {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super();
|
||||
assert(typeof hook === 'object');
|
||||
|
||||
this.logger = logger;
|
||||
this.url = hook.url;
|
||||
|
||||
this.username = hook.username;
|
||||
this.password = hook.password;
|
||||
this.secret = secret;
|
||||
this.account_sid = account_sid;
|
||||
|
||||
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() {
|
||||
return alerter;
|
||||
}
|
||||
|
||||
close() {
|
||||
/* subclass responsibility */
|
||||
}
|
||||
|
||||
_computeSignature(payload, timestamp, secret) {
|
||||
assert(secret);
|
||||
const data = `${timestamp}.${JSON.stringify(payload)}`;
|
||||
return crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(data, 'utf8')
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
_generateSigHeader(payload, secret) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const signature = this._computeSignature(payload, timestamp, secret);
|
||||
const scheme = 'v1';
|
||||
return {
|
||||
'Jambonz-Signature': `t=${timestamp},${scheme}=${signature}`
|
||||
};
|
||||
}
|
||||
|
||||
_isAbsoluteUrl(u) {
|
||||
return typeof u === 'string' &&
|
||||
u.startsWith('https://') || u.startsWith('http://') ||
|
||||
u.startsWith('ws://') || u.startsWith('wss://');
|
||||
}
|
||||
_isRelativeUrl(u) {
|
||||
return typeof u === 'string' && u.startsWith('/');
|
||||
}
|
||||
_roundTrip(startAt) {
|
||||
const diff = process.hrtime(startAt);
|
||||
const time = diff[0] * 1e3 + diff[1] * 1e-6;
|
||||
return time.toFixed(0);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = BaseRequestor;
|
||||
@@ -2,7 +2,6 @@
|
||||
"TaskName": {
|
||||
"Cognigy": "cognigy",
|
||||
"Conference": "conference",
|
||||
"Config": "config",
|
||||
"Dequeue": "dequeue",
|
||||
"Dial": "dial",
|
||||
"Dialogflow": "dialogflow",
|
||||
@@ -106,15 +105,6 @@
|
||||
"Hangup": "hangup",
|
||||
"Replaced": "replaced"
|
||||
},
|
||||
"HookMsgTypes": [
|
||||
"session:new",
|
||||
"session:reconnect",
|
||||
"session:redirect",
|
||||
"call:status",
|
||||
"queue:status",
|
||||
"verb:hook",
|
||||
"jambonz:error"
|
||||
],
|
||||
"MAX_SIMRINGS": 10,
|
||||
"BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)",
|
||||
"FS_UUID_SET_NAME": "fsUUIDs"
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
const {execSync} = require('child_process');
|
||||
const now = Date.now();
|
||||
const fsInventory = process.env.JAMBONES_FREESWITCH
|
||||
.split(',')
|
||||
.map((fs) => {
|
||||
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
|
||||
const opts = {address: arr[1], port: arr[2], secret: arr[3]};
|
||||
if (arr.length > 4) opts.advertisedAddress = arr[4];
|
||||
if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
|
||||
return opts;
|
||||
});
|
||||
|
||||
const clearChannels = () => {
|
||||
const {logger} = require('../..');
|
||||
const pwd = fsInventory[0].secret;
|
||||
const maxDurationMins = process.env.JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS || 180;
|
||||
|
||||
const calls = execSync(`/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "show calls"`, {encoding: 'utf8'})
|
||||
.split('\n')
|
||||
.filter((line) => line.match(/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{8}/))
|
||||
.map((line) => {
|
||||
const arr = line.split(',');
|
||||
const dt = new Date(arr[2]);
|
||||
const duration = (now - dt.getTime()) / 1000;
|
||||
return {
|
||||
uuid: arr[0],
|
||||
time: arr[2],
|
||||
duration
|
||||
};
|
||||
})
|
||||
.filter((c) => c.duration > 60 * maxDurationMins);
|
||||
|
||||
if (calls.length > 0) {
|
||||
logger.debug(`clearChannels: clearing ${calls.length} old calls longer than ${maxDurationMins} mins`);
|
||||
for (const call of calls) {
|
||||
const cmd = `/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "uuid_kill ${call.uuid}"`;
|
||||
const out = execSync(cmd, {encoding: 'utf8'});
|
||||
logger.debug({out}, 'clearChannels: command output');
|
||||
}
|
||||
}
|
||||
return calls.length;
|
||||
};
|
||||
|
||||
const clearFiles = () => {
|
||||
const {logger} = require('../..');
|
||||
const out = execSync('find /tmp -name "*.mp3" -mtime +2 -exec rm {} \\;');
|
||||
logger.debug({out}, 'clearFiles: command output');
|
||||
};
|
||||
|
||||
|
||||
module.exports = {clearChannels, clearFiles};
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
const bent = require('bent');
|
||||
const parseUrl = require('parse-url');
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
|
||||
function basicAuth(username, password) {
|
||||
if (!username || !password) return {};
|
||||
const creds = `${username}:${password || ''}`;
|
||||
const header = `Basic ${toBase64(creds)}`;
|
||||
return {Authorization: header};
|
||||
}
|
||||
|
||||
|
||||
class HttpRequestor extends BaseRequestor {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super(logger, account_sid, hook, secret);
|
||||
|
||||
this.method = hook.method || 'POST';
|
||||
this.authHeader = basicAuth(hook.username, hook.password);
|
||||
|
||||
const u = parseUrl(this.url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
const baseUrl = this._baseUrl = `${u.protocol}://${u.resource}${myPort}`;
|
||||
|
||||
this.get = bent(baseUrl, 'GET', 'buffer', 200, 201);
|
||||
this.post = bent(baseUrl, 'POST', 'buffer', 200, 201);
|
||||
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
assert(['GET', 'POST'].includes(this.method));
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return this._baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request.
|
||||
* All requests use json bodies.
|
||||
* All requests expect a 200 statusCode on success
|
||||
* @param {object|string} hook - may be a absolute or relative url, or an object
|
||||
* @param {string} [hook.url] - an absolute or relative url
|
||||
* @param {string} [hook.method] - 'GET' or 'POST'
|
||||
* @param {string} [hook.username] - if basic auth is protecting the endpoint
|
||||
* @param {string} [hook.password] - if basic auth is protecting the endpoint
|
||||
* @param {object} [params] - request parameters
|
||||
*/
|
||||
async request(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';
|
||||
|
||||
assert.ok(url, 'HttpRequestor:request url was not provided');
|
||||
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
|
||||
const {url: urlInfo = hook, method: methodInfo = 'POST'} = hook; // mask user/pass
|
||||
this.logger.debug({url: urlInfo, method: methodInfo, payload}, `HttpRequestor:request ${method} ${url}`);
|
||||
const startAt = process.hrtime();
|
||||
|
||||
let buf;
|
||||
try {
|
||||
const sigHeader = this._generateSigHeader(payload, this.secret);
|
||||
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);
|
||||
} catch (err) {
|
||||
if (err.statusCode) {
|
||||
this.logger.info({baseUrl: this.baseUrl, url},
|
||||
`web callback returned unexpected status code ${err.statusCode}`);
|
||||
}
|
||||
else {
|
||||
this.logger.error({err, baseUrl: this.baseUrl, url},
|
||||
'web callback returned unexpected error');
|
||||
}
|
||||
let opts = {account_sid: this.account_sid};
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
|
||||
}
|
||||
else if (err.name === 'StatusError') {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_STATUS_FAILURE, url, status: err.statusCode};
|
||||
}
|
||||
else {
|
||||
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url, detail: err.message};
|
||||
}
|
||||
this.Alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
|
||||
|
||||
throw err;
|
||||
}
|
||||
const rtt = this._roundTrip(startAt);
|
||||
if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']);
|
||||
|
||||
if (buf && buf.toString().length > 0) {
|
||||
try {
|
||||
const json = JSON.parse(buf.toString());
|
||||
this.logger.info({response: json}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
|
||||
return json;
|
||||
}
|
||||
catch (err) {
|
||||
//this.logger.debug({err, url, method}, `HttpRequestor:request returned non-JSON content: '${buf.toString()}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HttpRequestor;
|
||||
@@ -60,16 +60,10 @@ class SingleDialer extends Emitter {
|
||||
opts.headers = opts.headers || {};
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
...(this.target.headers || {}),
|
||||
'X-Jambonz-Routing': this.target.type,
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
'X-Call-Sid': this.callSid
|
||||
};
|
||||
if (srf.locals.fsUUID) {
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
};
|
||||
}
|
||||
this.ms = ms;
|
||||
let uri, to;
|
||||
try {
|
||||
@@ -150,7 +144,6 @@ class SingleDialer extends Emitter {
|
||||
* (a) create a CallInfo for this call
|
||||
* (a) create a logger for this call
|
||||
*/
|
||||
req.srf = srf;
|
||||
this.callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
parentCallInfo: this.parentCallInfo,
|
||||
@@ -164,14 +157,10 @@ class SingleDialer extends Emitter {
|
||||
callId: this.callInfo.callId
|
||||
});
|
||||
this.inviteInProgress = req;
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Trying,
|
||||
sipStatus: 100,
|
||||
sipReason: 'Trying'
|
||||
});
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
},
|
||||
cbProvisional: (prov) => {
|
||||
const status = {sipStatus: prov.status, sipReason: prov.reason};
|
||||
const status = {sipStatus: prov.status};
|
||||
if ([180, 183].includes(prov.status) && prov.body) {
|
||||
if (status.callStatus !== CallStatus.EarlyMedia) {
|
||||
status.callStatus = CallStatus.EarlyMedia;
|
||||
@@ -186,11 +175,7 @@ class SingleDialer extends Emitter {
|
||||
await connectStream(this.dlg.remote.sdp);
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.inviteInProgress = null;
|
||||
this.emit('callStatusChange', {
|
||||
sipStatus: 200,
|
||||
sipReason: 'OK',
|
||||
callStatus: CallStatus.InProgress
|
||||
});
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`);
|
||||
const connectTime = this.dlg.connectTime = moment();
|
||||
|
||||
@@ -198,12 +183,7 @@ class SingleDialer extends Emitter {
|
||||
if (this.killed) {
|
||||
this.logger.info(`SingleDialer:exec race condition - we were killed as call connected: ${this.callSid}`);
|
||||
const duration = moment().diff(connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Completed,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Request Terminated',
|
||||
duration
|
||||
});
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
if (this.ep) this.ep.destroy();
|
||||
return;
|
||||
}
|
||||
@@ -230,19 +210,14 @@ class SingleDialer extends Emitter {
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
})
|
||||
.on('refer', (req, res) => {
|
||||
this.emit('refer', this.callInfo, req, res);
|
||||
});
|
||||
|
||||
if (this.confirmHook) this._executeApp(this.confirmHook);
|
||||
else this.emit('accept');
|
||||
} catch (err) {
|
||||
this.inviteInProgress = null;
|
||||
const status = {callStatus: CallStatus.Failed};
|
||||
if (err instanceof SipError) {
|
||||
status.sipStatus = err.status;
|
||||
status.sipReason = err.reason;
|
||||
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}`);
|
||||
@@ -365,16 +340,16 @@ class SingleDialer extends Emitter {
|
||||
});
|
||||
}
|
||||
|
||||
_notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
|
||||
if (this.callInfo) {
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.requestor.request('call:status', this.application.call_status_hook, this.callInfo.toJSON());
|
||||
this.requestor.request(this.application.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
this.logger.info(err, `SingleDialer:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,6 @@ module.exports = (logger) => {
|
||||
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
}
|
||||
else if (process.env.K8S && process.env.K8S_SBC_SIP_SERVICE_NAME) {
|
||||
sbcs = [`${process.env.K8S_SBC_SIP_SERVICE_NAME}:5060`];
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
}
|
||||
|
||||
// listen for SNS lifecycle changes
|
||||
let lifecycleEmitter = new Emitter();
|
||||
@@ -32,10 +28,6 @@ module.exports = (logger) => {
|
||||
lifecycleEmitter = await require('./aws-sns-lifecycle')(logger);
|
||||
|
||||
lifecycleEmitter
|
||||
.on('SubscriptionConfirmation', ({publicIp}) => {
|
||||
const {srf} = require('../..');
|
||||
srf.locals.publicIp = publicIp;
|
||||
})
|
||||
.on(LifeCycleEvents.ScaleIn, () => {
|
||||
logger.info('AWS scale-in notification: begin drying up calls');
|
||||
dryUpCalls = true;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module.exports = function(tasks) {
|
||||
return `[${tasks.map((t) => t.summary).join(',')}]`;
|
||||
return `[${tasks.map((t) => t.name).join(',')}]`;
|
||||
};
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const short = require('short-uuid');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const Websocket = require('ws');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const HttpRequestor = require('./http-requestor');
|
||||
const MAX_RECONNECTS = 5;
|
||||
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
|
||||
|
||||
class WsRequestor extends BaseRequestor {
|
||||
constructor(logger, account_sid, hook, secret) {
|
||||
super(logger, account_sid, hook, secret);
|
||||
this.connections = 0;
|
||||
this.messagesInFlight = new Map();
|
||||
this.maliciousClient = false;
|
||||
this.closedByUs = false;
|
||||
|
||||
assert(this._isAbsoluteUrl(this.url));
|
||||
|
||||
this.on('socket-closed', this._onSocketClosed.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON payload over the websocket. If this is the first request,
|
||||
* open the websocket.
|
||||
* All requests expect an ack message in response
|
||||
* @param {object|string} hook - may be a absolute or relative url, or an object
|
||||
* @param {string} [hook.url] - an absolute or relative url
|
||||
* @param {string} [hook.method] - 'GET' or 'POST'
|
||||
* @param {string} [hook.username] - if basic auth is protecting the endpoint
|
||||
* @param {string} [hook.password] - if basic auth is protecting the endpoint
|
||||
* @param {object} [params] - request parameters
|
||||
*/
|
||||
async request(type, hook, params) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
const url = hook.url || hook;
|
||||
|
||||
if (this.maliciousClient) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'session:new') this.call_sid = params.callSid;
|
||||
|
||||
/* 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, hook, this.secret);
|
||||
return requestor.request(type, hook, params);
|
||||
}
|
||||
|
||||
/* connect if necessary */
|
||||
if (!this.ws) {
|
||||
if (this.connections >= MAX_RECONNECTS) {
|
||||
throw new Error(`max attempts connecting to ${this.url}`);
|
||||
}
|
||||
try {
|
||||
const startAt = process.hrtime();
|
||||
await this._connect();
|
||||
const rtt = this._roundTrip(startAt);
|
||||
this.stats.histogram('app.hook.connect_time', rtt, ['hook_type:app']);
|
||||
} catch (err) {
|
||||
this.logger.info({url, err}, 'WsRequestor:request - failed connecting');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
assert(this.ws);
|
||||
|
||||
/* prepare and send message */
|
||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||
|
||||
const msgid = short.generate();
|
||||
const obj = {
|
||||
type,
|
||||
msgid,
|
||||
call_sid: this.call_sid,
|
||||
hook: type === 'verb:hook' ? url : undefined,
|
||||
data: {...payload}
|
||||
};
|
||||
|
||||
//this.logger.debug({obj}, `websocket: sending (${url})`);
|
||||
|
||||
/* simple notifications */
|
||||
if (['call:status', 'jambonz:error'].includes(type)) {
|
||||
this.ws.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/* messages that require an ack */
|
||||
return new Promise((resolve, reject) => {
|
||||
/* give the far end a reasonable amount of time to ack our message */
|
||||
const timer = setTimeout(() => {
|
||||
const {failure} = this.messagesInFlight.get(msgid);
|
||||
failure && failure(`timeout from far end for msgid ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
}, RESPONSE_TIMEOUT_MS);
|
||||
|
||||
/* save the message info for reply */
|
||||
const startAt = process.hrtime();
|
||||
this.messagesInFlight.set(msgid, {
|
||||
success: (response) => {
|
||||
clearTimeout(timer);
|
||||
const rtt = this._roundTrip(startAt);
|
||||
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);
|
||||
},
|
||||
failure: (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
/* send the message */
|
||||
this.ws.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.logger.info('WsRequestor:close closing socket');
|
||||
this.closedByUs = true;
|
||||
try {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws.removeAllListeners();
|
||||
}
|
||||
|
||||
for (const [msgid, obj] of this.messagesInFlight) {
|
||||
obj.failure(`abandoning msgid ${msgid} since we have closed the socket`);
|
||||
}
|
||||
this.messagesInFlight.clear();
|
||||
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'WsRequestor: Error closing socket');
|
||||
}
|
||||
}
|
||||
|
||||
_connect() {
|
||||
assert(!this.ws);
|
||||
return new Promise((resolve, reject) => {
|
||||
let opts = {
|
||||
followRedirects: true,
|
||||
maxRedirects: 2,
|
||||
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++ > 0) this.request('session:reconnect', this.url);
|
||||
resolve();
|
||||
})
|
||||
.once('not-ready', (err) => {
|
||||
this.removeAllListeners('ready');
|
||||
reject(err);
|
||||
});
|
||||
const ws = new Websocket(this.url, ['ws.jambonz.org'], opts);
|
||||
this._setHandlers(ws);
|
||||
});
|
||||
}
|
||||
|
||||
_setHandlers(ws) {
|
||||
ws
|
||||
.once('open', this._onOpen.bind(this, ws))
|
||||
.once('close', this._onClose.bind(this))
|
||||
.on('message', this._onMessage.bind(this))
|
||||
.once('unexpected-response', this._onUnexpectedResponse.bind(this, ws))
|
||||
.on('error', this._onError.bind(this));
|
||||
}
|
||||
|
||||
_onError(err) {
|
||||
if (this.connections > 0) {
|
||||
this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
|
||||
}
|
||||
else this.emit('not-ready', err);
|
||||
}
|
||||
|
||||
_onOpen(ws) {
|
||||
if (this.ws) this.logger.info({old_ws: this.ws._socket.address()}, 'WsRequestor:_onOpen');
|
||||
assert(!this.ws);
|
||||
this.emit('ready', ws);
|
||||
this.logger.info({url: this.url}, 'WsRequestor - successfully connected');
|
||||
}
|
||||
|
||||
_onClose() {
|
||||
if (this.connections > 0) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
||||
this.emit('socket-closed');
|
||||
}
|
||||
this.ws && this.ws.removeAllListeners();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
_onUnexpectedResponse(ws, req, res) {
|
||||
assert(!this.ws);
|
||||
this.logger.info({
|
||||
headers: res.headers,
|
||||
statusCode: res.statusCode,
|
||||
statusMessage: res.statusMessage
|
||||
}, 'WsRequestor - unexpected response');
|
||||
this.emit('connection-failure');
|
||||
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
|
||||
}
|
||||
|
||||
_onSocketClosed() {
|
||||
this.ws = null;
|
||||
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedByUs) {
|
||||
setTimeout(this._connect.bind(this), 500);
|
||||
}
|
||||
}
|
||||
|
||||
_onMessage(content, isBinary) {
|
||||
if (this.isBinary) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:_onMessage - discarding binary message');
|
||||
this.maliciousClient = true;
|
||||
this.ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
/* messages must be JSON format */
|
||||
try {
|
||||
const obj = JSON.parse(content);
|
||||
const {type, msgid, command, queueCommand = false, data} = obj;
|
||||
|
||||
this.logger.debug({obj}, 'WsRequestor:request websocket: received');
|
||||
assert.ok(type, 'type property not supplied');
|
||||
|
||||
switch (type) {
|
||||
case 'ack':
|
||||
assert.ok(msgid, 'msgid not supplied');
|
||||
this._recvAck(msgid, data);
|
||||
break;
|
||||
|
||||
case 'command':
|
||||
assert.ok(command, 'command property not supplied');
|
||||
assert.ok(data, 'data property not supplied');
|
||||
this._recvCommand(msgid, command, queueCommand, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert.ok(false, `invalid type property: ${type}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'WsRequestor:_onMessage - invalid incoming message');
|
||||
}
|
||||
}
|
||||
|
||||
_recvAck(msgid, data) {
|
||||
const obj = this.messagesInFlight.get(msgid);
|
||||
if (!obj) {
|
||||
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
|
||||
this.messagesInFlight.delete(msgid);
|
||||
const {success} = obj;
|
||||
success && success(data);
|
||||
}
|
||||
|
||||
_recvCommand(msgid, command, queueCommand, data) {
|
||||
// TODO: validate command
|
||||
this.logger.info({msgid, command, queueCommand, data}, 'received command');
|
||||
this.emit('command', {msgid, command, queueCommand, data});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WsRequestor;
|
||||
248
package-lock.json
generated
248
package-lock.json
generated
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.3",
|
||||
"version": "v0.7.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.3",
|
||||
"version": "v0.7.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cognigy/socket-client": "^4.5.5",
|
||||
"@jambonz/db-helpers": "^0.6.16",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.26",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.19",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"aws-sdk": "^2.1073.0",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.2",
|
||||
@@ -26,14 +26,14 @@
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.13.4",
|
||||
"short-uuid": "^4.2.0",
|
||||
"pino": "^6.13.2",
|
||||
"queue": "^6.0.2",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
"ws": "^8.5.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -46,10 +46,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
"utf-8-validate": "^5.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -552,16 +548,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/realtimedb-helpers": {
|
||||
"version": "0.4.26",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.26.tgz",
|
||||
"integrity": "sha512-vGNC5IsYj7Qj7mOfgfti0/ZouYs+9GHEX4v7w3JguPW1NqUbvFayPs3xo49di3NJsHV0NnKWx2rMgrZzvWTgTA==",
|
||||
"version": "0.4.19",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.19.tgz",
|
||||
"integrity": "sha512-wEc7OXogQ5SZ9mgLNxviBNY0WvUKGGztyDzze3oi44edsZp0vNneVHUmdZuDzuDvUMBqsvCa0fvri8LGaxovlw==",
|
||||
"dependencies": {
|
||||
"@google-cloud/text-to-speech": "^3.4.0",
|
||||
"@jambonz/promisify-redis": "^0.0.6",
|
||||
"aws-sdk": "^2.1072.0",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.3",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.20.0",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.19.0",
|
||||
"redis": "^3.1.2"
|
||||
}
|
||||
},
|
||||
@@ -914,14 +910,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-sdk": {
|
||||
"version": "2.1073.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1073.0.tgz",
|
||||
"integrity": "sha512-TtyHDL4ZEs8Zh/DqWY/hv745DTWrIwOyBAvfjBJ45RE9h0TjpWqCIowEtb6gRPAKyPPyfGH4s+rEYu07vNK1Hg==",
|
||||
"version": "2.1060.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1060.0.tgz",
|
||||
"integrity": "sha512-c734/CZiVSsuVnEkx/7dodI8ndgOnxiCTwwlEFlMxdAZfLLJplteFwi6c/J2GZQktvz2ysV/HVtNKcJkasYJzw==",
|
||||
"dependencies": {
|
||||
"buffer": "4.9.2",
|
||||
"events": "1.1.1",
|
||||
"ieee754": "1.1.13",
|
||||
"jmespath": "0.16.0",
|
||||
"jmespath": "0.15.0",
|
||||
"querystring": "0.2.0",
|
||||
"sax": "1.2.1",
|
||||
"url": "0.10.3",
|
||||
@@ -1105,19 +1101,6 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"node_modules/bufferutil": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz",
|
||||
"integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
|
||||
@@ -2270,6 +2253,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig=="
|
||||
},
|
||||
"node_modules/fastify-warning": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
|
||||
"integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw=="
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
|
||||
@@ -3325,9 +3313,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jmespath": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
|
||||
"integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
|
||||
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@@ -3525,9 +3513,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/microsoft-cognitiveservices-speech-sdk": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.20.0.tgz",
|
||||
"integrity": "sha512-/KWNHQb36nhIkBr6HClh+T+gHicgmC5JKJhCnQmL6XGAAndLtew0aRbh7cMK10IFNTgozGKiejhrfnwiYH3IBA==",
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.19.0.tgz",
|
||||
"integrity": "sha512-FYyXdJcTLX8Iunnn9XHzTfR+43YdEZn1q89KX6Vzqtaa4hz/LemdC9Zni50VIfkXMNtq2sJFYf5wKXvxm1wQHA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^6.0.1",
|
||||
@@ -3539,7 +3527,7 @@
|
||||
"simple-lru-cache": "0.0.2",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^8.3.0",
|
||||
"ws": "^7.5.6"
|
||||
"ws": "^7.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/microsoft-cognitiveservices-speech-sdk/node_modules/https-proxy-agent": {
|
||||
@@ -3562,26 +3550,6 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/microsoft-cognitiveservices-speech-sdk/node_modules/ws": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
@@ -3765,17 +3733,6 @@
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
|
||||
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-noop": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-noop/-/node-noop-0.0.1.tgz",
|
||||
@@ -4092,15 +4049,15 @@
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "6.13.4",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.4.tgz",
|
||||
"integrity": "sha512-g4tHSISmQJYUEKEMVdaZ+ZokWwFnTwZL5JPn+lnBVZ1BuBbrSchrXwQINknkM5+Q4fF6U9NjiI8PWwwMDHt9zA==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz",
|
||||
"integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==",
|
||||
"dependencies": {
|
||||
"fast-redact": "^3.0.0",
|
||||
"fast-safe-stringify": "^2.0.8",
|
||||
"fastify-warning": "^0.2.0",
|
||||
"flatstr": "^1.0.12",
|
||||
"pino-std-serializers": "^3.1.0",
|
||||
"process-warning": "^1.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"sonic-boom": "^1.0.2"
|
||||
},
|
||||
@@ -4145,11 +4102,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
|
||||
"integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -4243,6 +4195,15 @@
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://cognigy.pkgs.visualstudio.com/_packaging/cognigy-feed/npm/registry/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha1-uRUlKD4jFcdVPS76GNg+dkMv7WU=",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz",
|
||||
@@ -5164,9 +5125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
@@ -5177,19 +5138,6 @@
|
||||
"resolved": "https://registry.npmjs.org/username-sync/-/username-sync-1.0.3.tgz",
|
||||
"integrity": "sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA=="
|
||||
},
|
||||
"node_modules/utf-8-validate": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.8.tgz",
|
||||
"integrity": "sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -5406,11 +5354,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
|
||||
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
@@ -5939,16 +5887,16 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/realtimedb-helpers": {
|
||||
"version": "0.4.26",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.26.tgz",
|
||||
"integrity": "sha512-vGNC5IsYj7Qj7mOfgfti0/ZouYs+9GHEX4v7w3JguPW1NqUbvFayPs3xo49di3NJsHV0NnKWx2rMgrZzvWTgTA==",
|
||||
"version": "0.4.19",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.4.19.tgz",
|
||||
"integrity": "sha512-wEc7OXogQ5SZ9mgLNxviBNY0WvUKGGztyDzze3oi44edsZp0vNneVHUmdZuDzuDvUMBqsvCa0fvri8LGaxovlw==",
|
||||
"requires": {
|
||||
"@google-cloud/text-to-speech": "^3.4.0",
|
||||
"@jambonz/promisify-redis": "^0.0.6",
|
||||
"aws-sdk": "^2.1072.0",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.3",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.20.0",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.19.0",
|
||||
"redis": "^3.1.2"
|
||||
}
|
||||
},
|
||||
@@ -6241,14 +6189,14 @@
|
||||
}
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.1073.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1073.0.tgz",
|
||||
"integrity": "sha512-TtyHDL4ZEs8Zh/DqWY/hv745DTWrIwOyBAvfjBJ45RE9h0TjpWqCIowEtb6gRPAKyPPyfGH4s+rEYu07vNK1Hg==",
|
||||
"version": "2.1060.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1060.0.tgz",
|
||||
"integrity": "sha512-c734/CZiVSsuVnEkx/7dodI8ndgOnxiCTwwlEFlMxdAZfLLJplteFwi6c/J2GZQktvz2ysV/HVtNKcJkasYJzw==",
|
||||
"requires": {
|
||||
"buffer": "4.9.2",
|
||||
"events": "1.1.1",
|
||||
"ieee754": "1.1.13",
|
||||
"jmespath": "0.16.0",
|
||||
"jmespath": "0.15.0",
|
||||
"querystring": "0.2.0",
|
||||
"sax": "1.2.1",
|
||||
"url": "0.10.3",
|
||||
@@ -6397,15 +6345,6 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"bufferutil": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz",
|
||||
"integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
|
||||
@@ -7339,6 +7278,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig=="
|
||||
},
|
||||
"fastify-warning": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
|
||||
"integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw=="
|
||||
},
|
||||
"file-entry-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
|
||||
@@ -8126,9 +8070,9 @@
|
||||
}
|
||||
},
|
||||
"jmespath": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
|
||||
"integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
|
||||
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
@@ -8284,9 +8228,9 @@
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"microsoft-cognitiveservices-speech-sdk": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.20.0.tgz",
|
||||
"integrity": "sha512-/KWNHQb36nhIkBr6HClh+T+gHicgmC5JKJhCnQmL6XGAAndLtew0aRbh7cMK10IFNTgozGKiejhrfnwiYH3IBA==",
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.19.0.tgz",
|
||||
"integrity": "sha512-FYyXdJcTLX8Iunnn9XHzTfR+43YdEZn1q89KX6Vzqtaa4hz/LemdC9Zni50VIfkXMNtq2sJFYf5wKXvxm1wQHA==",
|
||||
"requires": {
|
||||
"agent-base": "^6.0.1",
|
||||
"asn1.js-rfc2560": "^5.0.1",
|
||||
@@ -8297,7 +8241,7 @@
|
||||
"simple-lru-cache": "0.0.2",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^8.3.0",
|
||||
"ws": "^7.5.6"
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"https-proxy-agent": {
|
||||
@@ -8315,12 +8259,6 @@
|
||||
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8461,12 +8399,6 @@
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w=="
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
|
||||
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==",
|
||||
"optional": true
|
||||
},
|
||||
"node-noop": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-noop/-/node-noop-0.0.1.tgz",
|
||||
@@ -8711,15 +8643,15 @@
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"pino": {
|
||||
"version": "6.13.4",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.4.tgz",
|
||||
"integrity": "sha512-g4tHSISmQJYUEKEMVdaZ+ZokWwFnTwZL5JPn+lnBVZ1BuBbrSchrXwQINknkM5+Q4fF6U9NjiI8PWwwMDHt9zA==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz",
|
||||
"integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==",
|
||||
"requires": {
|
||||
"fast-redact": "^3.0.0",
|
||||
"fast-safe-stringify": "^2.0.8",
|
||||
"fastify-warning": "^0.2.0",
|
||||
"flatstr": "^1.0.12",
|
||||
"pino-std-serializers": "^3.1.0",
|
||||
"process-warning": "^1.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"sonic-boom": "^1.0.2"
|
||||
}
|
||||
@@ -8752,11 +8684,6 @@
|
||||
"fromentries": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"process-warning": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
|
||||
"integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -8829,6 +8756,14 @@
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://cognigy.pkgs.visualstudio.com/_packaging/cognigy-feed/npm/registry/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha1-uRUlKD4jFcdVPS76GNg+dkMv7WU=",
|
||||
"requires": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"quick-format-unescaped": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz",
|
||||
@@ -9560,9 +9495,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
@@ -9573,15 +9508,6 @@
|
||||
"resolved": "https://registry.npmjs.org/username-sync/-/username-sync-1.0.3.tgz",
|
||||
"integrity": "sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA=="
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.8.tgz",
|
||||
"integrity": "sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -9755,9 +9681,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
|
||||
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml2js": {
|
||||
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "v0.7.3",
|
||||
"version": "v0.7.2",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -21,19 +21,20 @@
|
||||
},
|
||||
"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=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/ ",
|
||||
"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=debug 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"
|
||||
"jslint": "eslint app.js lib",
|
||||
"jslint:fix": "eslint app.js lib --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cognigy/socket-client": "^4.5.5",
|
||||
"@jambonz/db-helpers": "^0.6.16",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.26",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.19",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"aws-sdk": "^2.1073.0",
|
||||
"aws-sdk": "^2.1060.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.2",
|
||||
@@ -43,14 +44,14 @@
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.13.4",
|
||||
"short-uuid": "^4.2.0",
|
||||
"pino": "^6.13.2",
|
||||
"queue": "^6.0.2",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
"ws": "^8.5.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,9 +61,5 @@
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
"utf-8-validate": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user