mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
23 Commits
feat/queue
...
v0.8.4-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91fe3ceb06 | ||
|
|
a7d07ce7ae | ||
|
|
7cd6c27f90 | ||
|
|
aad24744f3 | ||
|
|
ab0452879e | ||
|
|
ffdb7a0bb5 | ||
|
|
354818b974 | ||
|
|
30beb9c093 | ||
|
|
b978b3bc2f | ||
|
|
a1c38f8a2e | ||
|
|
37f3668016 | ||
|
|
55935e3f35 | ||
|
|
b7070121ee | ||
|
|
01260ad054 | ||
|
|
bd911c88f9 | ||
|
|
d96712a8d6 | ||
|
|
fdd8f7e743 | ||
|
|
bb852600c0 | ||
|
|
210bbcbdbf | ||
|
|
5910dbf0d3 | ||
|
|
90468ffe48 | ||
|
|
863c4dfa34 | ||
|
|
484be8442c |
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -2,6 +2,8 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
@@ -18,7 +20,7 @@ jobs:
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
run: |
|
||||
IMAGE_ID=feature-server
|
||||
IMAGE_ID=jambonz/feature-server
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
4
app.js
4
app.js
@@ -20,7 +20,9 @@ const tracer = require('./tracer')(JAMBONES_OTEL_SERVICE_NAME);
|
||||
const api = require('@opentelemetry/api');
|
||||
srf.locals = {...srf.locals, otel: {tracer, api}};
|
||||
|
||||
const opts = {level: JAMBONES_LOGLEVEL};
|
||||
const opts = {
|
||||
level: JAMBONES_LOGLEVEL
|
||||
};
|
||||
const pino = require('pino');
|
||||
const logger = pino(opts, pino.destination({sync: false}));
|
||||
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
|
||||
|
||||
@@ -8,7 +8,12 @@ const checkEnvs = () => {
|
||||
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
|
||||
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
||||
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
if (process.env.JAMBONES_REDIS_SENTINELS) {
|
||||
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
|
||||
} else {
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
}
|
||||
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
|
||||
};
|
||||
|
||||
@@ -51,7 +56,7 @@ const JAMBONES_SBCS = process.env.JAMBONES_SBCS;
|
||||
/* websockets */
|
||||
const JAMBONES_WS_HANDSHAKE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS, 10) || 1500;
|
||||
const JAMBONES_WS_MAX_PAYLOAD = parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD, 10) || 24 * 1024;
|
||||
|
||||
const JAMBONES_WS_PING_INTERVAL_MS = parseInt(process.env.JAMBONES_WS_PING_INTERVAL_MS, 10) || 0;
|
||||
const MAX_RECONNECTS = 5;
|
||||
const RESPONSE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT, 10) || 5000;
|
||||
|
||||
@@ -119,6 +124,30 @@ const HTTP_TIMEOUT = 10000;
|
||||
|
||||
const OPTIONS_PING_INTERVAL = parseInt(process.env.OPTIONS_PING_INTERVAL, 10) || 30000;
|
||||
|
||||
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
|
||||
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
|
||||
let host, port = 26379;
|
||||
if (sentinel.includes(':')) {
|
||||
const arr = sentinel.split(':');
|
||||
host = arr[0];
|
||||
port = parseInt(arr[1], 10);
|
||||
} else {
|
||||
host = sentinel;
|
||||
}
|
||||
return {host, port};
|
||||
}),
|
||||
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
|
||||
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
|
||||
}),
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
|
||||
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
|
||||
})
|
||||
} : null;
|
||||
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL;
|
||||
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME;
|
||||
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD;
|
||||
|
||||
module.exports = {
|
||||
JAMBONES_MYSQL_HOST,
|
||||
JAMBONES_MYSQL_USER,
|
||||
@@ -138,6 +167,7 @@ module.exports = {
|
||||
JAMBONES_FREESWITCH,
|
||||
JAMBONES_REDIS_HOST,
|
||||
JAMBONES_REDIS_PORT,
|
||||
JAMBONES_REDIS_SENTINELS,
|
||||
SMPP_URL,
|
||||
JAMBONES_NETWORK_CIDR,
|
||||
JAMBONES_API_BASE_URL,
|
||||
@@ -186,10 +216,14 @@ module.exports = {
|
||||
RESPONSE_TIMEOUT_MS,
|
||||
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
|
||||
JAMBONES_WS_MAX_PAYLOAD,
|
||||
JAMBONES_WS_PING_INTERVAL_MS,
|
||||
MAX_RECONNECTS,
|
||||
GCP_JSON_KEY,
|
||||
MICROSOFT_REGION,
|
||||
MICROSOFT_API_KEY,
|
||||
SONIOX_API_KEY,
|
||||
DEEPGRAM_API_KEY
|
||||
DEEPGRAM_API_KEY,
|
||||
JAMBONZ_RECORD_WS_BASE_URL,
|
||||
JAMBONZ_RECORD_WS_USERNAME,
|
||||
JAMBONZ_RECORD_WS_PASSWORD
|
||||
};
|
||||
|
||||
@@ -19,8 +19,16 @@ router.post('/', async(req, res) => {
|
||||
logger.debug({body: req.body}, 'got createCall request');
|
||||
try {
|
||||
let uri, cs, to;
|
||||
// app_json is creaeted by only api-server.
|
||||
// if it available, take it and delete before creating task
|
||||
const app_json = req.body.app_json;
|
||||
delete req.body.app_json;
|
||||
const restDial = makeTask(logger, {'rest:dial': req.body});
|
||||
const {lookupAccountDetails} = dbUtils(logger, srf);
|
||||
restDial.appJson = app_json;
|
||||
const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
|
||||
const {
|
||||
lookupAppBySid
|
||||
} = srf.locals.dbHelpers;
|
||||
const {getSBC, getFreeswitch} = srf.locals;
|
||||
const sbcAddress = getSBC();
|
||||
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
||||
@@ -36,6 +44,9 @@ router.post('/', async(req, res) => {
|
||||
const account = await lookupAccountBySid(req.body.account_sid);
|
||||
const accountInfo = await lookupAccountDetails(req.body.account_sid);
|
||||
const callSid = uuidv4();
|
||||
const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null;
|
||||
const record_all_calls = account.record_all_calls || (application && application.record_all_calls);
|
||||
const recordOutputFormat = account.record_format || 'mp3';
|
||||
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
@@ -43,7 +54,9 @@ router.post('/', async(req, res) => {
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
'X-Call-Sid': callSid,
|
||||
'X-Account-Sid': accountSid,
|
||||
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost})
|
||||
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
||||
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
||||
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat})
|
||||
};
|
||||
|
||||
switch (target.type) {
|
||||
@@ -77,7 +90,6 @@ router.post('/', async(req, res) => {
|
||||
}
|
||||
|
||||
if (target.type === 'phone' && target.trunk) {
|
||||
const {lookupCarrier} = dbUtils(this.logger, srf);
|
||||
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
||||
logger.info(
|
||||
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
||||
@@ -91,10 +103,11 @@ router.post('/', async(req, res) => {
|
||||
* check if from-number matches any existing numbers on Jambonz
|
||||
* */
|
||||
if (target.type === 'phone' && !target.trunk) {
|
||||
const {lookupCarrierByPhoneNumber} = dbUtils(this.logger, srf);
|
||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, restDial.from);
|
||||
const str = restDial.from || '';
|
||||
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
|
||||
logger.info(
|
||||
`createCall: selected ${voip_carrier_sid} for requested phone number: ${restDial.from || 'unspecified'})`);
|
||||
`createCall: selected ${voip_carrier_sid} for requested phone number: ${callingNumber || 'unspecified'})`);
|
||||
if (voip_carrier_sid) {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const dbUtils = require('./utils/db-utils');
|
||||
const RootSpan = require('./utils/call-tracer');
|
||||
const listTaskNames = require('./utils/summarize-tasks');
|
||||
const {
|
||||
JAMBONES_MYSQL_REFRESH_TTL,
|
||||
JAMBONES_MYSQL_REFRESH_TTL
|
||||
} = require('./config');
|
||||
|
||||
module.exports = function(srf, logger) {
|
||||
@@ -322,6 +322,7 @@ module.exports = function(srf, logger) {
|
||||
const httpHeaders = b3 && { b3 };
|
||||
json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
||||
}
|
||||
|
||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
span?.setAttributes({
|
||||
'http.statusCode': 200,
|
||||
|
||||
@@ -19,7 +19,10 @@ const HttpRequestor = require('../utils/http-requestor');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
const {
|
||||
JAMBONES_INJECT_CONTENT,
|
||||
AWS_REGION
|
||||
AWS_REGION,
|
||||
JAMBONZ_RECORD_WS_BASE_URL,
|
||||
JAMBONZ_RECORD_WS_USERNAME,
|
||||
JAMBONZ_RECORD_WS_PASSWORD,
|
||||
} = require('../config');
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
||||
@@ -132,6 +135,11 @@ class CallSession extends Emitter {
|
||||
return this.callInfo.callStatus;
|
||||
}
|
||||
|
||||
get isBackGroundListen() {
|
||||
return !(this.backgroundListenTask === null ||
|
||||
this.backgroundListenTask === undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* SIP call-id for the call
|
||||
*/
|
||||
@@ -495,7 +503,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
async startBackgroundListen(opts) {
|
||||
async startBackgroundListen(opts, bugname) {
|
||||
if (this.isListenEnabled) {
|
||||
this.logger.info('CallSession:startBackgroundListen - listen is already enabled, ignoring request');
|
||||
return;
|
||||
@@ -504,8 +512,9 @@ class CallSession extends Emitter {
|
||||
this.logger.debug({opts}, 'CallSession:startBackgroundListen');
|
||||
const t = normalizeJambones(this.logger, [opts]);
|
||||
this.backgroundListenTask = makeTask(this.logger, t[0]);
|
||||
this.backgroundListenTask.bugname = bugname;
|
||||
const resources = await this._evaluatePreconditions(this.backgroundListenTask);
|
||||
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundListenTask.summary}`);
|
||||
const {span, ctx} = this.rootSpan.startChildSpan(`background-listen:${this.backgroundListenTask.summary}`);
|
||||
this.backgroundListenTask.span = span;
|
||||
this.backgroundListenTask.ctx = ctx;
|
||||
this.backgroundListenTask.exec(this, resources)
|
||||
@@ -528,6 +537,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
async stopBackgroundListen() {
|
||||
this.logger.debug('CallSession:stopBackgroundListen');
|
||||
try {
|
||||
if (this.backgroundListenTask) {
|
||||
this.backgroundListenTask.removeAllListeners();
|
||||
@@ -536,7 +546,6 @@ class CallSession extends Emitter {
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'CallSession:stopBackgroundListen - Error stopping listen task');
|
||||
}
|
||||
this.backgroundListenTask = null;
|
||||
}
|
||||
|
||||
async enableBotMode(gather, autoEnable) {
|
||||
@@ -1341,7 +1350,10 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
// we are going from an early media connection to answer
|
||||
await this.propagateAnswer();
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
// only do this for inbound call.
|
||||
await this.propagateAnswer();
|
||||
}
|
||||
return {
|
||||
...resources,
|
||||
...(this.isSipRecCallSession && {ep2: this.ep2})
|
||||
@@ -1512,7 +1524,6 @@ class CallSession extends Emitter {
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -1749,6 +1760,13 @@ class CallSession extends Emitter {
|
||||
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
if (this.callMoved) return;
|
||||
|
||||
if (callStatus === CallStatus.InProgress) {
|
||||
// nice, call is in progress, good time to enable record
|
||||
await this.enableRecordAllCall();
|
||||
} else if (callStatus == CallStatus.Completed && this.isBackGroundListen) {
|
||||
await this.stopBackgroundListen();
|
||||
}
|
||||
|
||||
/* race condition: we hang up at the same time as the caller */
|
||||
if (callStatus === CallStatus.Completed) {
|
||||
if (this.notifiedComplete) return;
|
||||
@@ -1779,6 +1797,23 @@ class CallSession extends Emitter {
|
||||
.catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
|
||||
async enableRecordAllCall() {
|
||||
if (this.accountInfo.account.record_all_calls || this.application.record_all_calls) {
|
||||
const listenOpts = {
|
||||
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.accountInfo.account.bucket_credential.vendor}`,
|
||||
wsAuth: {
|
||||
username: JAMBONZ_RECORD_WS_USERNAME,
|
||||
password: JAMBONZ_RECORD_WS_PASSWORD
|
||||
},
|
||||
mixType : 'stereo',
|
||||
passDtmf: true
|
||||
};
|
||||
|
||||
this.logger.debug({listenOpts}, 'Record all calls: enabling listen');
|
||||
await this.startBackgroundListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* notifyTaskError - only used when websocket connection is used instead of webhooks
|
||||
*/
|
||||
|
||||
@@ -114,7 +114,12 @@ class Conference extends Task {
|
||||
}
|
||||
this.emitter.emit('kill');
|
||||
await this._doFinalMemberCheck(cs);
|
||||
if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ;
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.ep.conn.removeAllListeners('esl::event::CUSTOM::*');
|
||||
this.ep.api(`conference ${this.confName} kick ${this.memberId}`)
|
||||
.catch((err) => this.logger.info({err}, 'Error kicking participant'));
|
||||
}
|
||||
cs.clearConferenceDetails();
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class TaskDequeue extends Task {
|
||||
this.queueName = this.data.name;
|
||||
this.timeout = this.data.timeout || 0;
|
||||
this.beep = this.data.beep === true;
|
||||
this.callSid = this.data.callSid;
|
||||
|
||||
this.emitter = new Emitter();
|
||||
this.state = DequeueResults.Timeout;
|
||||
@@ -53,7 +54,7 @@ class TaskDequeue extends Task {
|
||||
}
|
||||
|
||||
_getMemberFromQueue(cs) {
|
||||
const {popFront} = cs.srf.locals.dbHelpers;
|
||||
const {retrieveFromSortedSet, retrieveByPatternSortedSet} = cs.srf.locals.dbHelpers;
|
||||
|
||||
return new Promise(async(resolve) => {
|
||||
let timer;
|
||||
@@ -70,7 +71,12 @@ class TaskDequeue extends Task {
|
||||
|
||||
do {
|
||||
try {
|
||||
const url = await popFront(this.queueName);
|
||||
let url;
|
||||
if (this.callSid) {
|
||||
url = await retrieveByPatternSortedSet(this.queueName, `*${this.callSid}`);
|
||||
} else {
|
||||
url = await retrieveFromSortedSet(this.queueName);
|
||||
}
|
||||
if (url) {
|
||||
found = true;
|
||||
clearTimeout(timer);
|
||||
@@ -78,7 +84,7 @@ class TaskDequeue extends Task {
|
||||
resolve(url);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error popFront');
|
||||
this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error Sorted Set');
|
||||
}
|
||||
await sleepFor(5000);
|
||||
} while (!this.killed && !timedout && !found);
|
||||
|
||||
@@ -137,6 +137,7 @@ class TaskDial extends Task {
|
||||
|
||||
get canReleaseMedia() {
|
||||
const keepAnchor = this.data.anchorMedia ||
|
||||
this.cs.isBackGroundListen ||
|
||||
ANCHOR_MEDIA_ALWAYS ||
|
||||
this.listenTask ||
|
||||
this.transcribeTask ||
|
||||
@@ -166,6 +167,16 @@ class TaskDial extends Task {
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
try {
|
||||
if (this.listenTask) {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.listenTask.summary}`);
|
||||
this.listenTask.span = span;
|
||||
this.listenTask.ctx = ctx;
|
||||
}
|
||||
if (this.transcribeTask) {
|
||||
const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`);
|
||||
this.transcribeTask.span = span;
|
||||
this.transcribeTask.ctx = ctx;
|
||||
}
|
||||
if (this.data.amd) {
|
||||
this.startAmd = cs.startAmd;
|
||||
this.stopAmd = cs.stopAmd;
|
||||
@@ -223,10 +234,12 @@ class TaskDial extends Task {
|
||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||
if (this.listenTask) {
|
||||
await this.listenTask.kill(cs);
|
||||
this.listenTask.span.end();
|
||||
this.listenTask = null;
|
||||
}
|
||||
if (this.transcribeTask) {
|
||||
await this.transcribeTask.kill(cs);
|
||||
this.transcribeTask.span.end();
|
||||
this.transcribeTask = null;
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
@@ -409,6 +422,7 @@ class TaskDial extends Task {
|
||||
'X-Account-Sid': cs.accountSid,
|
||||
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
|
||||
...(req && req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
|
||||
...(req && req.has('X-Voip-Carrier-Sid') && {'X-Voip-Carrier-Sid': req.get('X-Voip-Carrier-Sid')}),
|
||||
// Put headers at the end to make sure opt.headers override all default behavior.
|
||||
...this.headers
|
||||
};
|
||||
@@ -459,7 +473,7 @@ class TaskDial extends Task {
|
||||
}
|
||||
if (t.type === 'phone' && t.trunk) {
|
||||
const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk);
|
||||
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk})`);
|
||||
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk}`);
|
||||
if (voip_carrier_sid) {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
@@ -470,9 +484,11 @@ class TaskDial extends Task {
|
||||
* check if number matches any existing numbers
|
||||
* */
|
||||
if (t.type === 'phone' && !t.trunk) {
|
||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, t.number);
|
||||
const str = this.callerId || req.callingNumber || '';
|
||||
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
|
||||
this.logger.info(
|
||||
`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested phone number: ${t.number})`);
|
||||
`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested phone number: ${callingNumber}`);
|
||||
if (voip_carrier_sid) {
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
}
|
||||
@@ -653,7 +669,7 @@ class TaskDial extends Task {
|
||||
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
||||
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep2: this.epOther, ep:this.ep});
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep});
|
||||
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther});
|
||||
if (this.startAmd) {
|
||||
try {
|
||||
|
||||
@@ -222,6 +222,7 @@ class Dialogflow extends Task {
|
||||
|
||||
try {
|
||||
const obj = {
|
||||
account_sid: cs.accountSid,
|
||||
text: intent.fulfillmentText,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
|
||||
@@ -18,6 +18,7 @@ class TaskEnqueue extends Task {
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.queueName = this.data.name;
|
||||
this.priority = this.data.priority;
|
||||
this.waitHook = this.data.waitHook;
|
||||
|
||||
this.emitter = new Emitter();
|
||||
@@ -70,12 +71,22 @@ class TaskEnqueue extends Task {
|
||||
}
|
||||
|
||||
async _addToQueue(cs, dlg) {
|
||||
const {pushBack} = cs.srf.locals.dbHelpers;
|
||||
const {addToSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||
const url = getUrl(cs);
|
||||
this.waitStartTime = Date.now();
|
||||
this.logger.debug({queue: this.queueName, url}, 'pushing url onto queue');
|
||||
const members = await pushBack(this.queueName, url);
|
||||
this.logger.info(`TaskEnqueue:_addToQueue: added to queue, length now ${members}`);
|
||||
if (this.priority < 0) {
|
||||
this.logger.warn(`priority ${this.priority} is invalid, need to be non-negative integer,
|
||||
999 will be used for priority`);
|
||||
}
|
||||
let members = await addToSortedSet(this.queueName, url, this.priority);
|
||||
if (members === 1) {
|
||||
this.logger.info('TaskEnqueue:_addToQueue: added to queue');
|
||||
} else {
|
||||
this.logger.info('TaskEnqueue:_addToQueue: failed to add to queue');
|
||||
}
|
||||
members = await sortedSetLength(this.queueName);
|
||||
|
||||
this.notifyUrl = url;
|
||||
|
||||
/* invoke account-level webhook for queue event notifications */
|
||||
@@ -90,9 +101,9 @@ class TaskEnqueue extends Task {
|
||||
}
|
||||
|
||||
async _removeFromQueue(cs) {
|
||||
const {removeFromList, lengthOfList} = cs.srf.locals.dbHelpers;
|
||||
await removeFromList(this.queueName, getUrl(cs));
|
||||
return await lengthOfList(this.queueName);
|
||||
const {retrieveByPatternSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||
await retrieveByPatternSortedSet(this.queueName, `*${getUrl(cs)}`);
|
||||
return await sortedSetLength(this.queueName);
|
||||
}
|
||||
|
||||
async performAction() {
|
||||
@@ -279,13 +290,13 @@ class TaskEnqueue extends Task {
|
||||
this.emitter.emit('dequeue', opts);
|
||||
|
||||
try {
|
||||
const {lengthOfList} = cs.srf.locals.dbHelpers;
|
||||
const members = await lengthOfList(this.queueName);
|
||||
const {sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||
const members = await sortedSetLength(this.queueName);
|
||||
this.dequeued = true;
|
||||
cs.performQueueWebhook({
|
||||
event: 'leave',
|
||||
queue: this.data.name,
|
||||
length: Math.max(members - 1, 0),
|
||||
length: Math.max(members, 0),
|
||||
leaveReason: 'dequeued',
|
||||
leaveTime: Date.now(),
|
||||
dequeuer: opts.dequeuer
|
||||
@@ -301,7 +312,7 @@ class TaskEnqueue extends Task {
|
||||
}
|
||||
|
||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) {
|
||||
const {lengthOfList, getListPosition} = cs.srf.locals.dbHelpers;
|
||||
const {sortedSetLength, sortedSetPositionByPattern} = cs.srf.locals.dbHelpers;
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
|
||||
@@ -313,9 +324,14 @@ class TaskEnqueue extends Task {
|
||||
queueTime: getElapsedTime(this.waitStartTime)
|
||||
};
|
||||
try {
|
||||
const queueSize = await lengthOfList(this.queueName);
|
||||
const queuePosition = await getListPosition(this.queueName, this.notifyUrl);
|
||||
Object.assign(params, {queueSize, queuePosition});
|
||||
const queueSize = await sortedSetLength(this.queueName);
|
||||
const queuePosition = await sortedSetPositionByPattern(this.queueName, `*${this.notifyUrl}`);
|
||||
Object.assign(params, {
|
||||
queueSize,
|
||||
queuePosition: queuePosition.length ? queuePosition[0] : 0,
|
||||
callSid: this.cs.callSid,
|
||||
callId: this.cs.callId,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
|
||||
}
|
||||
|
||||
@@ -817,7 +817,11 @@ class TaskGather extends Task {
|
||||
this.logger.debug({evt}, 'TaskGather:resolve buffered results');
|
||||
}
|
||||
|
||||
this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
|
||||
this.span.setAttributes({
|
||||
channel: 1,
|
||||
'stt.resolve': reason,
|
||||
'stt.result': JSON.stringify(evt)
|
||||
});
|
||||
if (this.needsStt && this.ep && this.ep.connected) {
|
||||
this.ep.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
|
||||
|
||||
@@ -189,6 +189,7 @@ class Lex extends Task {
|
||||
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
account_sid: cs.accountSid,
|
||||
text: msg,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
|
||||
@@ -3,6 +3,7 @@ const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../ut
|
||||
const makeTask = require('./make_task');
|
||||
const moment = require('moment');
|
||||
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
|
||||
const DTMF_SPAN_NAME = 'dtmf';
|
||||
|
||||
class TaskListen extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
@@ -29,6 +30,8 @@ class TaskListen extends Task {
|
||||
|
||||
get name() { return TaskName.Listen; }
|
||||
|
||||
set bugname(name) { this._bugname = name; }
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
@@ -65,7 +68,8 @@ class TaskListen extends Task {
|
||||
if (this.ep && this.ep.connected) {
|
||||
this.logger.debug('TaskListen:kill closing websocket');
|
||||
try {
|
||||
await this.ep.forkAudioStop();
|
||||
const args = this._bugname ? [this._bugname] : [];
|
||||
await this.ep.forkAudioStop(...args);
|
||||
this.logger.debug('TaskListen:kill successfully closed websocket');
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'TaskListen:kill');
|
||||
@@ -85,13 +89,16 @@ class TaskListen extends Task {
|
||||
|
||||
async updateListen(status) {
|
||||
if (!this.killed && this.ep && this.ep.connected) {
|
||||
const args = this._bugname ? [this._bugname] : [];
|
||||
this.logger.info(`TaskListen:updateListen status ${status}`);
|
||||
switch (status) {
|
||||
case ListenStatus.Pause:
|
||||
await this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
||||
await this.ep.forkAudioPause(...args)
|
||||
.catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
||||
break;
|
||||
case ListenStatus.Resume:
|
||||
await this.ep.forkAudioResume().catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
||||
await this.ep.forkAudioResume(...args)
|
||||
.catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -120,6 +127,7 @@ class TaskListen extends Task {
|
||||
wsUrl: this.hook.url,
|
||||
mixType: this.mixType,
|
||||
sampling: this.sampleRate,
|
||||
...(this._bugname && {bugname: this._bugname}),
|
||||
metadata
|
||||
});
|
||||
this.recordStartTime = moment();
|
||||
@@ -161,12 +169,25 @@ class TaskListen extends Task {
|
||||
}
|
||||
|
||||
_onDtmf(ep, evt) {
|
||||
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${evt.dtmf}`);
|
||||
const {dtmf, duration} = evt;
|
||||
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${dtmf}`);
|
||||
if (this.passDtmf && this.ep?.connected) {
|
||||
const obj = {event: 'dtmf', dtmf: evt.dtmf, duration: evt.duration};
|
||||
this.ep.forkAudioSendText(obj)
|
||||
const obj = {event: 'dtmf', dtmf, duration};
|
||||
const args = this._bugname ? [this._bugname, obj] : [obj];
|
||||
this.ep.forkAudioSendText(...args)
|
||||
.catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf'));
|
||||
}
|
||||
|
||||
/* add a child span for the dtmf event */
|
||||
const msDuration = Math.floor((duration / 8000) * 1000);
|
||||
const {span} = this.startChildSpan(`${DTMF_SPAN_NAME}:${dtmf}`);
|
||||
span.setAttributes({
|
||||
channel: 1,
|
||||
dtmf,
|
||||
duration: `${msDuration}ms`
|
||||
});
|
||||
span.end();
|
||||
|
||||
if (evt.dtmf === this.finishOnKey) {
|
||||
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
||||
this.results.digits = evt.dtmf;
|
||||
@@ -192,7 +213,15 @@ class TaskListen extends Task {
|
||||
try {
|
||||
const results = await ep.play(evt.file);
|
||||
logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
||||
ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
|
||||
const obj = {
|
||||
type: 'playDone',
|
||||
data: {
|
||||
id: evt.id,
|
||||
...results
|
||||
}
|
||||
};
|
||||
const args = this._bugname ? [this._bugname, obj] : [obj];
|
||||
ep.forkAudioSendText(...args);
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error playing file');
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ class TaskRestDial extends Task {
|
||||
|
||||
get name() { return TaskName.RestDial; }
|
||||
|
||||
set appJson(app_json) {
|
||||
this.app_json = app_json;
|
||||
}
|
||||
|
||||
/**
|
||||
* INVITE has just been sent at this point
|
||||
*/
|
||||
@@ -84,7 +88,12 @@ class TaskRestDial extends Task {
|
||||
this.logger.info({err}, 'Rest:dial:Call established - Error calling startAmd');
|
||||
}
|
||||
}
|
||||
const tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
|
||||
let tasks;
|
||||
if (this.app_json) {
|
||||
tasks = JSON.parse(this.app_json);
|
||||
} else {
|
||||
tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
|
||||
}
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
||||
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||
|
||||
@@ -120,6 +120,7 @@ class TaskSay extends Task {
|
||||
});
|
||||
try {
|
||||
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
|
||||
account_sid: cs.accountSid,
|
||||
text,
|
||||
vendor,
|
||||
language,
|
||||
|
||||
@@ -14,6 +14,8 @@ const {
|
||||
} = require('../utils/constants');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
const STT_LISTEN_SPAN_NAME = 'stt-listen';
|
||||
|
||||
class TaskTranscribe extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts);
|
||||
@@ -35,20 +37,25 @@ class TaskTranscribe extends Task {
|
||||
this.transcriptionHook = this.data.transcriptionHook;
|
||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
this.interim = !!recognizer.interim;
|
||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
this.interim = !!recognizer.interim;
|
||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
||||
|
||||
this.data.recognizer.hints = this.data.recognizer.hints || [];
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
|
||||
}
|
||||
else this.data.recognizer = {hints: [], altLanguages: []};
|
||||
|
||||
/* buffer for soniox transcripts */
|
||||
this._sonioxTranscripts = [];
|
||||
|
||||
recognizer.hints = recognizer.hints || [];
|
||||
recognizer.altLanguages = recognizer.altLanguages || [];
|
||||
this.childSpan = [null, null];
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
@@ -236,6 +243,10 @@ class TaskTranscribe extends Task {
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
|
||||
await this._transcribe(ep);
|
||||
|
||||
/* start child span for this channel */
|
||||
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||
this.childSpan[channel - 1] = {span, ctx};
|
||||
}
|
||||
|
||||
async _transcribe(ep) {
|
||||
@@ -285,6 +296,16 @@ class TaskTranscribe extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
/* we've got a transcript, so end the otel child span for this channel */
|
||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||
this.childSpan[channel - 1].span.setAttributes({
|
||||
channel,
|
||||
'stt.resolve': 'transcript',
|
||||
'stt.result': JSON.stringify(evt)
|
||||
});
|
||||
this.childSpan[channel - 1].span.end();
|
||||
}
|
||||
|
||||
if (this.transcriptionHook) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
@@ -315,16 +336,44 @@ class TaskTranscribe extends Task {
|
||||
this._clearTimer();
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
else {
|
||||
/* start another child span for this channel */
|
||||
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||
this.childSpan[channel - 1] = {span, ctx};
|
||||
}
|
||||
}
|
||||
|
||||
_onNoAudio(cs, ep, channel) {
|
||||
this.logger.debug(`TaskTranscribe:_onNoAudio restarting transcription on channel ${channel}`);
|
||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||
this.childSpan[channel - 1].span.setAttributes({
|
||||
channel,
|
||||
'stt.resolve': 'timeout'
|
||||
});
|
||||
this.childSpan[channel - 1].span.end();
|
||||
}
|
||||
this._transcribe(ep);
|
||||
|
||||
/* start new child span for this channel */
|
||||
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||
this.childSpan[channel - 1] = {span, ctx};
|
||||
}
|
||||
|
||||
_onMaxDurationExceeded(cs, ep, channel) {
|
||||
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded restarting transcription on channel ${channel}`);
|
||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||
this.childSpan[channel - 1].span.setAttributes({
|
||||
channel,
|
||||
'stt.resolve': 'max duration exceeded'
|
||||
});
|
||||
this.childSpan[channel - 1].span.end();
|
||||
}
|
||||
|
||||
this._transcribe(ep);
|
||||
|
||||
/* start new child span for this channel */
|
||||
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||
this.childSpan[channel - 1] = {span, ctx};
|
||||
}
|
||||
|
||||
_clearTimer() {
|
||||
@@ -337,7 +386,7 @@ class TaskTranscribe extends Task {
|
||||
this.logger.debug('TaskTranscribe:_onDeepgramConnect');
|
||||
}
|
||||
|
||||
_onDeepGramConnectFailure(cs, _ep, _channel, evt) {
|
||||
_onDeepGramConnectFailure(cs, _ep, channel, evt) {
|
||||
const {reason} = evt;
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onDeepgramConnectFailure');
|
||||
@@ -348,6 +397,14 @@ class TaskTranscribe extends Task {
|
||||
vendor: 'deepgram',
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
|
||||
this.notifyError(`Failed connecting to speech vendor deepgram: ${reason}`);
|
||||
|
||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||
this.childSpan[channel - 1].span.setAttributes({
|
||||
channel,
|
||||
'stt.resolve': 'connection failure'
|
||||
});
|
||||
this.childSpan[channel - 1].span.end();
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -355,7 +412,7 @@ class TaskTranscribe extends Task {
|
||||
this.logger.debug('TaskTranscribe:_onIbmConnect');
|
||||
}
|
||||
|
||||
_onIbmConnectFailure(cs, _ep, _channel, evt) {
|
||||
_onIbmConnectFailure(cs, _ep, channel, evt) {
|
||||
const {reason} = evt;
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onIbmConnectFailure');
|
||||
@@ -366,6 +423,14 @@ class TaskTranscribe extends Task {
|
||||
vendor: 'ibm',
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
|
||||
this.notifyError(`Failed connecting to speech vendor IBM: ${reason}`);
|
||||
|
||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||
this.childSpan[channel - 1].span.setAttributes({
|
||||
channel,
|
||||
'stt.resolve': 'connection failure'
|
||||
});
|
||||
this.childSpan[channel - 1].span.end();
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
_onIbmError(cs, _ep, _channel, evt) {
|
||||
|
||||
@@ -94,6 +94,12 @@ const speechMapper = (cred) => {
|
||||
return obj;
|
||||
};
|
||||
|
||||
const bucketCredentialDecrypt = (account) => {
|
||||
const { bucket_credential } = account.account;
|
||||
if (!bucket_credential || bucket_credential.vendor) return;
|
||||
account.account.bucket_credential = JSON.parse(decrypt(bucket_credential));
|
||||
};
|
||||
|
||||
module.exports = (logger, srf) => {
|
||||
const {pool} = srf.locals.dbHelpers;
|
||||
const pp = pool.promise();
|
||||
@@ -112,9 +118,11 @@ module.exports = (logger, srf) => {
|
||||
speech.push(speechMapper(s));
|
||||
}
|
||||
});
|
||||
const account = r[0];
|
||||
bucketCredentialDecrypt(account);
|
||||
|
||||
return {
|
||||
...r[0],
|
||||
...account,
|
||||
speech
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ const {
|
||||
JAMBONES_FREESWITCH,
|
||||
JAMBONES_REDIS_HOST,
|
||||
JAMBONES_REDIS_PORT,
|
||||
JAMBONES_REDIS_SENTINELS,
|
||||
SMPP_URL,
|
||||
JAMBONES_TIME_SERIES_HOST,
|
||||
JAMBONES_ESL_LISTEN_ADDRESS,
|
||||
@@ -146,7 +147,7 @@ function installSrfLocals(srf, logger) {
|
||||
password: JAMBONES_MYSQL_PASSWORD,
|
||||
database: JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
}, logger, tracer);
|
||||
}, logger);
|
||||
const {
|
||||
client,
|
||||
updateCallStatus,
|
||||
@@ -167,7 +168,12 @@ function installSrfLocals(srf, logger) {
|
||||
removeFromList,
|
||||
getListPosition,
|
||||
lengthOfList,
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
addToSortedSet,
|
||||
retrieveFromSortedSet,
|
||||
retrieveByPatternSortedSet,
|
||||
sortedSetLength,
|
||||
sortedSetPositionByPattern
|
||||
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: JAMBONES_REDIS_HOST,
|
||||
port: JAMBONES_REDIS_PORT || 6379
|
||||
}, logger, tracer);
|
||||
@@ -175,7 +181,7 @@ function installSrfLocals(srf, logger) {
|
||||
synthAudio,
|
||||
getNuanceAccessToken,
|
||||
getIbmAccessToken,
|
||||
} = require('@jambonz/speech-utils')({
|
||||
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: JAMBONES_REDIS_HOST,
|
||||
port: JAMBONES_REDIS_PORT || 6379
|
||||
}, logger, tracer);
|
||||
@@ -228,7 +234,12 @@ function installSrfLocals(srf, logger) {
|
||||
lengthOfList,
|
||||
getListPosition,
|
||||
getNuanceAccessToken,
|
||||
getIbmAccessToken
|
||||
getIbmAccessToken,
|
||||
addToSortedSet,
|
||||
retrieveFromSortedSet,
|
||||
retrieveByPatternSortedSet,
|
||||
sortedSetLength,
|
||||
sortedSetPositionByPattern
|
||||
},
|
||||
parentLogger: logger,
|
||||
getSBC,
|
||||
|
||||
@@ -45,6 +45,10 @@ class SingleDialer extends Emitter {
|
||||
return this.callInfo.callStatus;
|
||||
}
|
||||
|
||||
get applicationSid() {
|
||||
return this.application?.application_sid || this.callInfo?.applicationSid;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used for all http requests within this session
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ const Websocket = require('ws');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const {
|
||||
RESPONSE_TIMEOUT_MS,
|
||||
JAMBONES_WS_PING_INTERVAL_MS,
|
||||
MAX_RECONNECTS,
|
||||
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
|
||||
JAMBONES_WS_MAX_PAYLOAD
|
||||
@@ -178,9 +179,17 @@ class WsRequestor extends BaseRequestor {
|
||||
});
|
||||
}
|
||||
|
||||
_stopPingTimer() {
|
||||
if (this._pingTimer) {
|
||||
clearInterval(this._pingTimer);
|
||||
this._pingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closedGracefully = true;
|
||||
this.logger.debug('WsRequestor:close closing socket');
|
||||
this._stopPingTimer();
|
||||
try {
|
||||
if (this.ws) {
|
||||
this.ws.close(1000);
|
||||
@@ -195,6 +204,7 @@ class WsRequestor extends BaseRequestor {
|
||||
|
||||
_connect() {
|
||||
assert(!this.ws);
|
||||
this._stopPingTimer();
|
||||
return new Promise((resolve, reject) => {
|
||||
const handshakeTimeout = JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ?
|
||||
parseInt(JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) :
|
||||
@@ -255,10 +265,15 @@ class WsRequestor extends BaseRequestor {
|
||||
this.connectInProgress = false;
|
||||
this.connections++;
|
||||
this.emit('ready', ws);
|
||||
|
||||
if (JAMBONES_WS_PING_INTERVAL_MS > 15000) {
|
||||
this._pingTimer = setInterval(() => this.ws?.ping(), JAMBONES_WS_PING_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
_onClose(code) {
|
||||
this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`);
|
||||
this._stopPingTimer();
|
||||
if (this.connections > 0 && code !== 1000) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
||||
this.emit('socket-closed');
|
||||
@@ -283,6 +298,7 @@ class WsRequestor extends BaseRequestor {
|
||||
_onSocketClosed() {
|
||||
this.ws = null;
|
||||
this.emit('connection-dropped');
|
||||
this._stopPingTimer();
|
||||
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
|
||||
if (!this._initMsgId) this._clearPendingMessages();
|
||||
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
|
||||
|
||||
2811
package-lock.json
generated
2811
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -25,13 +25,13 @@
|
||||
"jslint:fix": "eslint app.js tracer.js lib --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.8.1",
|
||||
"@jambonz/db-helpers": "^0.9.0",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.7.2",
|
||||
"@jambonz/speech-utils": "^0.0.13",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.6",
|
||||
"@jambonz/speech-utils": "^0.0.15",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@jambonz/verb-specifications": "^0.0.22",
|
||||
"@jambonz/time-series": "^0.2.7",
|
||||
"@jambonz/verb-specifications": "^0.0.24",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
|
||||
@@ -45,8 +45,8 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.21",
|
||||
"drachtio-srf": "^4.5.23",
|
||||
"drachtio-fsmrf": "^3.0.23",
|
||||
"drachtio-srf": "^4.5.26",
|
||||
"express": "^4.18.2",
|
||||
"ip": "^1.1.8",
|
||||
"moment": "^2.29.4",
|
||||
|
||||
@@ -39,7 +39,7 @@ test('\'config: listen\'', async(t) => {
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||
@@ -86,7 +86,7 @@ test('\'config: listen - stop\'', async(t) => {
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||
|
||||
@@ -92,7 +92,7 @@ test('test create-call call-hook basic authentication', async(t) => {
|
||||
"text": "hello"
|
||||
}
|
||||
];
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
//THEN
|
||||
await p;
|
||||
|
||||
@@ -150,7 +150,7 @@ test('test create-call amd', async(t) => {
|
||||
"length": 7
|
||||
}
|
||||
];
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
//THEN
|
||||
await p;
|
||||
|
||||
@@ -164,3 +164,59 @@ test('test create-call amd', async(t) => {
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('test create-call app_json', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
|
||||
|
||||
// GIVEN
|
||||
let from = 'create-call-app-json';
|
||||
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
|
||||
|
||||
// Give UAS app time to come up
|
||||
const p = sippUac('uas.xml', '172.38.0.10', from);
|
||||
await waitFor(1000);
|
||||
|
||||
const app_json = `[
|
||||
{
|
||||
"verb": "pause",
|
||||
"length": 7
|
||||
}
|
||||
]`;
|
||||
|
||||
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||
post('v1/createCall', {
|
||||
'account_sid':account_sid,
|
||||
"call_hook": {
|
||||
"url": "http://127.0.0.1:3100/",
|
||||
"method": "POST",
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
app_json,
|
||||
"from": from,
|
||||
"to": {
|
||||
"type": "phone",
|
||||
"number": "15583084809"
|
||||
},
|
||||
"amd": {
|
||||
"actionHook": "/actionHook"
|
||||
},
|
||||
"speech_recognizer_vendor": "google",
|
||||
"speech_recognizer_language": "en"
|
||||
});
|
||||
|
||||
//THEN
|
||||
await p;
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
/* SQLEditor (MySQL (2))*/
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS account_static_ips;
|
||||
@@ -16,6 +15,8 @@ DROP TABLE IF EXISTS call_routes;
|
||||
|
||||
DROP TABLE IF EXISTS dns_records;
|
||||
|
||||
DROP TABLE IF EXISTS lcr;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_routes;
|
||||
@@ -52,6 +53,8 @@ DROP TABLE IF EXISTS smpp_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS speech_credentials;
|
||||
|
||||
DROP TABLE IF EXISTS system_information;
|
||||
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_gateways;
|
||||
@@ -136,11 +139,23 @@ PRIMARY KEY (dns_record_sid)
|
||||
CREATE TABLE lcr_routes
|
||||
(
|
||||
lcr_route_sid CHAR(36),
|
||||
lcr_sid CHAR(36) NOT NULL,
|
||||
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
||||
description VARCHAR(1024),
|
||||
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
|
||||
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
) COMMENT='Least cost routing table';
|
||||
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
|
||||
|
||||
CREATE TABLE lcr
|
||||
(
|
||||
lcr_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
|
||||
service_provider_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
PRIMARY KEY (lcr_sid)
|
||||
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
|
||||
|
||||
CREATE TABLE password_settings
|
||||
(
|
||||
@@ -248,7 +263,10 @@ CREATE TABLE sbc_addresses
|
||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(255) NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 5060,
|
||||
tls_port INTEGER,
|
||||
wss_port INTEGER,
|
||||
service_provider_sid CHAR(36),
|
||||
last_updated DATETIME,
|
||||
PRIMARY KEY (sbc_address_sid)
|
||||
);
|
||||
|
||||
@@ -307,6 +325,13 @@ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (speech_credential_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE system_information
|
||||
(
|
||||
domain_name VARCHAR(255),
|
||||
sip_domain_name VARCHAR(255),
|
||||
monitoring_domain_name VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
user_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -357,6 +382,7 @@ smpp_inbound_password VARCHAR(64),
|
||||
register_from_user VARCHAR(128),
|
||||
register_from_domain VARCHAR(255),
|
||||
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
|
||||
register_status VARCHAR(4096),
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
@@ -403,6 +429,7 @@ inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound ca
|
||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
|
||||
PRIMARY KEY (sip_gateway_sid)
|
||||
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
||||
|
||||
@@ -435,7 +462,7 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
|
||||
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
|
||||
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
|
||||
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||
app_json VARCHAR(16384),
|
||||
app_json TEXT,
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
@@ -502,6 +529,14 @@ ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid)
|
||||
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
|
||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid);
|
||||
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
|
||||
|
||||
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
|
||||
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
|
||||
CREATE INDEX account_sid_idx ON lcr (account_sid);
|
||||
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
|
||||
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
|
||||
@@ -648,4 +683,4 @@ ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (devic
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
@@ -43,7 +43,7 @@ test('\'dial-phone\'', async(t) => {
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||
@@ -102,7 +102,7 @@ test('\'dial-sip\'', async(t) => {
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||
@@ -169,7 +169,7 @@ test('\'dial-user\'', async(t) => {
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||
|
||||
@@ -50,7 +50,7 @@ test('\'gather\' test - google', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -86,7 +86,7 @@ test('\'gather\' test - default (google)', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -133,7 +133,7 @@ test('\'config\' test - reset to app defaults', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -173,7 +173,7 @@ test('\'gather\' test - microsoft', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -213,7 +213,7 @@ test('\'gather\' test - aws', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -256,7 +256,7 @@ test('\'gather\' test - deepgram', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -298,7 +298,7 @@ test('\'gather\' test - soniox', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
|
||||
@@ -14,5 +14,6 @@ require('./play-tests');
|
||||
require('./sip-refer-tests');
|
||||
require('./listen-tests');
|
||||
require('./config-test');
|
||||
require('./queue-test');
|
||||
require('./remove-test-db');
|
||||
require('./docker_stop');
|
||||
@@ -35,7 +35,7 @@ test('\'listen-success\'', async(t) => {
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||
@@ -57,7 +57,7 @@ test('\'listen-success\'', async(t) => {
|
||||
}
|
||||
});
|
||||
|
||||
test('\'listen-maxLength\'', async(t) => {
|
||||
test.skip('\'listen-maxLength\'', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
try {
|
||||
@@ -69,13 +69,13 @@ test('\'listen-maxLength\'', async(t) => {
|
||||
{
|
||||
"verb": "listen",
|
||||
"url": `ws://172.38.0.60:3000/${from}`,
|
||||
"mixType" : "mixed",
|
||||
"mixType" : "stereo",
|
||||
"timeout": 2,
|
||||
"maxLength": 2
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
@@ -109,7 +109,7 @@ test('\'listen-pause-resume\'', async(t) => {
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
const p = sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
|
||||
@@ -33,7 +33,7 @@ test('\'play\' tests single link in plain text', async(t) => {
|
||||
];
|
||||
|
||||
const from = 'play_single_link';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
@@ -62,7 +62,7 @@ test('\'play\' tests multi links in array', async(t) => {
|
||||
];
|
||||
|
||||
const from = 'play_multi_links_in_array';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
@@ -100,8 +100,8 @@ test('\'play\' tests single link in conference', async(t) => {
|
||||
waitHook: `/customHook`
|
||||
}
|
||||
];
|
||||
provisionCustomHook(from, waitHookVerbs)
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCustomHook(from, waitHookVerbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
||||
@@ -141,8 +141,8 @@ test('\'play\' tests multi links in array in conference', async(t) => {
|
||||
waitHook: `/customHook`
|
||||
}
|
||||
];
|
||||
provisionCustomHook(from, waitHookVerbs)
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCustomHook(from, waitHookVerbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
||||
@@ -178,8 +178,8 @@ test('\'play\' tests with seekOffset and actionHook', async(t) => {
|
||||
const waitHookVerbs = [];
|
||||
|
||||
const from = 'play_action_hook';
|
||||
provisionCallHook(from, verbs)
|
||||
provisionCustomHook(from, waitHookVerbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
await provisionCustomHook(from, waitHookVerbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
@@ -218,7 +218,7 @@ test('\'play\' tests with earlymedia', async(t) => {
|
||||
];
|
||||
|
||||
const from = 'play_early_media';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-invite-expect-183-cancel.xml', '172.38.0.10', from);
|
||||
|
||||
127
test/queue-test.js
Normal file
127
test/queue-test.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const test = require('tape');
|
||||
const { sippUac } = require('./sipp')('test_fs');
|
||||
const clearModule = require('clear-module');
|
||||
const {provisionCallHook, provisionActionHook, provisionAnyHook} = require('./utils');
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('json');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
function connect(connectable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connectable.on('connect', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
||||
|
||||
test('\'enqueue-dequeue\' tests', async(t) => {
|
||||
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
const verbs = [
|
||||
{
|
||||
verb: 'enqueue',
|
||||
name: 'support',
|
||||
actionHook: '/actionHook'
|
||||
}
|
||||
];
|
||||
|
||||
const verbs2 = [
|
||||
{
|
||||
verb: 'dequeue',
|
||||
name: 'support'
|
||||
}
|
||||
];
|
||||
|
||||
const actionVerbs = [
|
||||
{
|
||||
verb: 'play',
|
||||
url: 'silence_stream://1000',
|
||||
earlyMedia: true
|
||||
}
|
||||
];
|
||||
|
||||
const from = 'enqueue_success';
|
||||
await provisionCallHook(from, verbs);
|
||||
await provisionActionHook(from, actionVerbs)
|
||||
|
||||
const from2 = 'dequeue_success';
|
||||
await provisionCallHook(from2, verbs2);
|
||||
|
||||
|
||||
// THEN
|
||||
const p1 = sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
|
||||
await sleepFor(1000);
|
||||
|
||||
const p2 = sippUac('uac-success-send-bye.xml', '172.38.0.11', from2);
|
||||
await Promise.all([p1, p2]);
|
||||
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.queue_result === 'bridged');
|
||||
t.pass('enqueue-dequeue: succeeds connect');
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\leave\' tests', async(t) => {
|
||||
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
const verbs = [
|
||||
{
|
||||
verb: 'enqueue',
|
||||
name: 'support1',
|
||||
waitHook: '/anyHook/enqueue_success_leave',
|
||||
actionHook: '/actionHook'
|
||||
}
|
||||
];
|
||||
|
||||
const anyHookVerbs = [
|
||||
{
|
||||
verb: 'leave'
|
||||
}
|
||||
];
|
||||
|
||||
const actionVerbs = [
|
||||
{
|
||||
verb: 'play',
|
||||
url: 'silence_stream://1000',
|
||||
earlyMedia: true
|
||||
}
|
||||
];
|
||||
|
||||
const from = 'enqueue_success_leave';
|
||||
await provisionCallHook(from, verbs);
|
||||
await provisionAnyHook(from, anyHookVerbs);
|
||||
await provisionActionHook(from, actionVerbs)
|
||||
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/enqueue_success_leave`);
|
||||
t.ok(obj.body.queue_position === 0);
|
||||
const obj1 = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj1.body.queue_result === 'leave');
|
||||
t.pass('enqueue-dequeue: succeeds connect');
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
@@ -31,7 +31,7 @@ test('\'say\' tests', async(t) => {
|
||||
];
|
||||
|
||||
const from = 'say_test_success';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
@@ -71,7 +71,7 @@ test('\'config\' reset synthesizer tests', async(t) => {
|
||||
];
|
||||
|
||||
const from = 'say_test_success';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
@@ -111,7 +111,7 @@ if (MICROSOFT_CUSTOM_API_KEY && MICROSOFT_DEPLOYMENT_ID && MICROSOFT_CUSTOM_REGI
|
||||
];
|
||||
|
||||
const from = 'say_test_success';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||
|
||||
@@ -41,8 +41,8 @@ test('\'refer\' tests w/202 and NOTIFY', {timeout: 25000}, async(t) => {
|
||||
const noVerbs = [];
|
||||
|
||||
const from = 'refer_with_notify';
|
||||
provisionCallHook(from, verbs);
|
||||
provisionActionHook(from, noVerbs)
|
||||
await provisionCallHook(from, verbs);
|
||||
await provisionActionHook(from, noVerbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-refer-with-notify.xml', '172.38.0.10', from);
|
||||
@@ -81,8 +81,8 @@ test('\'refer\' tests w/202 but no NOTIFY', {timeout: 25000}, async(t) => {
|
||||
const noVerbs = [];
|
||||
|
||||
const from = 'refer_no_notify';
|
||||
provisionCallHook(from, verbs);
|
||||
provisionActionHook(from, noVerbs)
|
||||
await provisionCallHook(from, verbs);
|
||||
await provisionActionHook(from, noVerbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-refer-no-notify.xml', '172.38.0.10', from);
|
||||
|
||||
@@ -40,7 +40,7 @@ test('sending SIP in-dialog requests tests', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "sip_indialog_test";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10', from);
|
||||
const obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
|
||||
@@ -36,7 +36,7 @@ obj.sippUac = (file, bindAddress, from='sipp', to='16174000000', loop=1) => {
|
||||
'-cid_str', `%u-%p@%s-${idx++}`,
|
||||
'172.38.0.50',
|
||||
'-key','from', from,
|
||||
'-key','to', to, '-trace_msg'
|
||||
'-key','to', to, '-trace_msg', '-trace_err'
|
||||
];
|
||||
|
||||
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
|
||||
|
||||
@@ -48,7 +48,7 @@ test('\'transcribe\' test - google', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -85,7 +85,7 @@ test('\'transcribe\' test - microsoft', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -122,7 +122,7 @@ test('\'transcribe\' test - aws', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -162,7 +162,7 @@ test('\'transcribe\' test - deepgram', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
@@ -202,7 +202,7 @@ test('\'transcribe\' test - soniox', async(t) => {
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
|
||||
@@ -6,31 +6,40 @@ const bent = require('bent');
|
||||
* The function help testcase to register desired jambonz json response for an application call
|
||||
* When a call has From number match the registered hook event, the desired jambonz json will be responded.
|
||||
*/
|
||||
const provisionCallHook = (from, verbs) => {
|
||||
const provisionCallHook = async (from, verbs) => {
|
||||
const mapping = {
|
||||
from,
|
||||
data: JSON.stringify(verbs)
|
||||
};
|
||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||
post('/appMapping', mapping);
|
||||
await post('/appMapping', mapping);
|
||||
}
|
||||
|
||||
const provisionCustomHook = (from, verbs) => {
|
||||
const provisionCustomHook = async(from, verbs) => {
|
||||
const mapping = {
|
||||
from,
|
||||
data: JSON.stringify(verbs)
|
||||
};
|
||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||
post(`/customHookMapping`, mapping);
|
||||
await post(`/customHookMapping`, mapping);
|
||||
}
|
||||
|
||||
const provisionActionHook = (from, verbs) => {
|
||||
const provisionActionHook = async(from, verbs) => {
|
||||
const mapping = {
|
||||
from,
|
||||
data: JSON.stringify(verbs)
|
||||
};
|
||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||
post(`/actionHook`, mapping);
|
||||
await post(`/actionHook`, mapping);
|
||||
}
|
||||
|
||||
module.exports = { provisionCallHook, provisionCustomHook, provisionActionHook}
|
||||
const provisionAnyHook = async(key, verbs) => {
|
||||
const mapping = {
|
||||
key,
|
||||
data: JSON.stringify(verbs)
|
||||
};
|
||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||
await post(`/anyHookMapping`, mapping);
|
||||
}
|
||||
|
||||
module.exports = { provisionCallHook, provisionCustomHook, provisionActionHook, provisionAnyHook}
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const app = express();
|
||||
const Websocket = require('ws');
|
||||
const listenPort = process.env.HTTP_PORT || 3000;
|
||||
const any_hook_json_mapping = new Map();
|
||||
let json_mapping = new Map();
|
||||
let hook_mapping = new Map();
|
||||
let ws_packet_count = new Map();
|
||||
@@ -61,7 +62,7 @@ app.all('/', (req, res) => {
|
||||
console.log(req.body, 'POST /');
|
||||
const key = req.body.from
|
||||
addRequestToMap(key, req, hook_mapping);
|
||||
return getJsonFromMap(key, req, res);
|
||||
return getJsonFromMap(json_mapping, key, req, res);
|
||||
});
|
||||
|
||||
app.post('/appMapping', (req, res) => {
|
||||
@@ -106,7 +107,7 @@ app.post('/actionHook', (req, res) => {
|
||||
app.all('/customHook', (req, res) => {
|
||||
let key = `${req.body.from}_customHook`;;
|
||||
console.log(req.body, `POST /customHook`);
|
||||
return getJsonFromMap(key, req, res);
|
||||
return getJsonFromMap(json_mapping, key, req, res);
|
||||
});
|
||||
|
||||
app.post('/customHookMapping', (req, res) => {
|
||||
@@ -116,6 +117,23 @@ app.post('/customHookMapping', (req, res) => {
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
/**
|
||||
* Any Hook
|
||||
*/
|
||||
|
||||
app.all('/anyHook/:key', (req, res) => {
|
||||
let key = req.params.key;
|
||||
console.log(req.body, `POST /anyHook/${key}`);
|
||||
return getJsonFromMap(any_hook_json_mapping, key, req, res);
|
||||
});
|
||||
|
||||
app.post('/anyHookMapping', (req, res) => {
|
||||
let key = req.body.key;
|
||||
console.log(req.body, `POST /anyHookMapping/${key}`);
|
||||
any_hook_json_mapping.set(key, req.body.data);
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
// Fetch Requests
|
||||
app.get('/requests/:key', (req, res) => {
|
||||
let key = req.params.key;
|
||||
@@ -162,9 +180,9 @@ app.get('/ws_metadata/:key', (req, res) => {
|
||||
* private function
|
||||
*/
|
||||
|
||||
function getJsonFromMap(key, req, res) {
|
||||
if (!json_mapping.has(key)) return res.sendStatus(404);
|
||||
const retData = JSON.parse(json_mapping.get(key));
|
||||
function getJsonFromMap(map, key, req, res) {
|
||||
if (!map.has(key)) return res.sendStatus(404);
|
||||
const retData = JSON.parse(map.get(key));
|
||||
console.log(retData, ` Response to ${req.method} ${req.url}`);
|
||||
addRequestToMap(key, req, hook_mapping);
|
||||
return res.json(retData);
|
||||
|
||||
@@ -45,7 +45,7 @@ test('basic webhook tests', async(t) => {
|
||||
];
|
||||
|
||||
const from = 'sip_decline_test_success';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
await sippUac('uac-expect-603.xml', '172.38.0.10', from);
|
||||
t.pass('webhook successfully declines call');
|
||||
@@ -73,7 +73,7 @@ test('invalid jambonz json create alert tests', async(t) => {
|
||||
};
|
||||
|
||||
const from = 'invalid_json_create_alert';
|
||||
provisionCallHook(from, verbs)
|
||||
await provisionCallHook(from, verbs)
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-invite-expect-480.xml', '172.38.0.10', from);
|
||||
|
||||
Reference in New Issue
Block a user