mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 08:40:38 +00:00
LCC: add conference hold and unhold actions
This commit is contained in:
@@ -34,7 +34,7 @@ class CallSession extends Emitter {
|
|||||||
* @param {array} opts.tasks - tasks we are to execute
|
* @param {array} opts.tasks - tasks we are to execute
|
||||||
* @param {callInfo} opts.callInfo - information about the call
|
* @param {callInfo} opts.callInfo - information about the call
|
||||||
*/
|
*/
|
||||||
constructor({logger, application, srf, tasks, callInfo, accountInfo}) {
|
constructor({logger, application, srf, tasks, callInfo, accountInfo, memberId, confName, confUuid}) {
|
||||||
super();
|
super();
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.application = application;
|
this.application = application;
|
||||||
@@ -42,6 +42,9 @@ class CallSession extends Emitter {
|
|||||||
this.callInfo = callInfo;
|
this.callInfo = callInfo;
|
||||||
this.accountInfo = accountInfo;
|
this.accountInfo = accountInfo;
|
||||||
this.tasks = tasks;
|
this.tasks = tasks;
|
||||||
|
this.memberId = memberId;
|
||||||
|
this.confName = confName;
|
||||||
|
this.confUuid = confUuid;
|
||||||
this.taskIdx = 0;
|
this.taskIdx = 0;
|
||||||
this.stackIdx = 0;
|
this.stackIdx = 0;
|
||||||
this.callGone = false;
|
this.callGone = false;
|
||||||
@@ -196,6 +199,27 @@ class CallSession extends Emitter {
|
|||||||
return this.accountInfo?.account?.webhook_secret;
|
return this.accountInfo?.account?.webhook_secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isInConference() {
|
||||||
|
return this.memberId && this.confName && this.confUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConferenceDetails(memberId, confName, confUuid) {
|
||||||
|
assert(!this.memberId && !this.confName && !this.confUuid);
|
||||||
|
assert (memberId && confName && confUuid);
|
||||||
|
|
||||||
|
this.logger.debug(`session is now in conference ${confName}:${memberId} - uuid ${confUuid}`);
|
||||||
|
this.memberId = memberId;
|
||||||
|
this.confName = confName;
|
||||||
|
this.confUuid = confUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearConferenceDetails() {
|
||||||
|
this.logger.debug(`session has now left conference ${this.confName}:${this.memberId}`);
|
||||||
|
this.memberId = null;
|
||||||
|
this.confName = null;
|
||||||
|
this.confUuid = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for speech credentials for the specified vendor
|
* Check for speech credentials for the specified vendor
|
||||||
* @param {*} vendor - google or aws
|
* @param {*} vendor - google or aws
|
||||||
@@ -436,15 +460,12 @@ class CallSession extends Emitter {
|
|||||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
|
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _lccConfHoldStatus(callSid, hold) {
|
async _lccConfHoldStatus(callSid, opts) {
|
||||||
this.logger.debug(`_lccConfHoldStatus ${hold}`);
|
|
||||||
const task = this.currentTask;
|
const task = this.currentTask;
|
||||||
if (!task || TaskName.Conference !== task.name) {
|
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
|
||||||
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as conference verb is not active');
|
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
|
||||||
}
|
}
|
||||||
// now do the mute/unmute, deaf/undeaf
|
task.doConferenceHold(this, opts);
|
||||||
task.mute(callSid, hold).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
|
|
||||||
task.deaf(callSid, hold).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -517,7 +538,7 @@ class CallSession extends Emitter {
|
|||||||
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
|
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
|
||||||
}
|
}
|
||||||
else if (opts.conf_hold_status) {
|
else if (opts.conf_hold_status) {
|
||||||
await this._lccConfHoldStatus(callSid, opts.conf_hold_status === 'hold');
|
await this._lccConfHoldStatus(callSid, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// whisper may be the only thing we are asked to do, or it may that
|
// whisper may be the only thing we are asked to do, or it may that
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const CallSession = require('./call-session');
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
class ConfirmCallSession extends CallSession {
|
class ConfirmCallSession extends CallSession {
|
||||||
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo}) {
|
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName}) {
|
||||||
super({
|
super({
|
||||||
logger,
|
logger,
|
||||||
application,
|
application,
|
||||||
@@ -16,7 +16,9 @@ class ConfirmCallSession extends CallSession {
|
|||||||
callSid: dlg.callSid,
|
callSid: dlg.callSid,
|
||||||
tasks,
|
tasks,
|
||||||
callInfo,
|
callInfo,
|
||||||
accountInfo
|
accountInfo,
|
||||||
|
memberId,
|
||||||
|
confName
|
||||||
});
|
});
|
||||||
this.dlg = dlg;
|
this.dlg = dlg;
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ function camelize(str) {
|
|||||||
|
|
||||||
function unhandled(logger, cs, evt) {
|
function unhandled(logger, cs, evt) {
|
||||||
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
||||||
logger.debug({evt}, `unhandled conference event: ${evt.getHeader('Action')}`) ;
|
// logger.debug({evt}, `unhandled conference event: ${evt.getHeader('Action')}`) ;
|
||||||
|
logger.debug(`unhandled conference event: ${evt.getHeader('Action')}`) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
function capitalize(s) {
|
function capitalize(s) {
|
||||||
@@ -213,6 +214,7 @@ class Conference extends Task {
|
|||||||
this._playSession.kill();
|
this._playSession.kill();
|
||||||
this._playSession = null;
|
this._playSession = null;
|
||||||
}
|
}
|
||||||
|
cs.clearConferenceDetails();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -335,6 +337,8 @@ class Conference extends Task {
|
|||||||
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
|
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
|
||||||
this.memberId = memberId;
|
this.memberId = memberId;
|
||||||
this.confUuid = confUuid;
|
this.confUuid = confUuid;
|
||||||
|
|
||||||
|
cs.setConferenceDetails(memberId, this.confName, confUuid);
|
||||||
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
|
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
|
||||||
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
|
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
|
||||||
this._notifyConferenceEvent(cs, 'join');
|
this._notifyConferenceEvent(cs, 'join');
|
||||||
@@ -375,22 +379,50 @@ class Conference extends Task {
|
|||||||
this.emitter.emit('join', opts);
|
this.emitter.emit('join', opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async mute(callSid, muted) {
|
async doConferenceHold(cs, opts) {
|
||||||
if (this.memberId) {
|
assert (cs.isInConference);
|
||||||
const prop = muted === true ? 'mute' : 'unmute';
|
|
||||||
this.ep.api(`conference ${this.confName} ${prop} ${this.memberId}`)
|
const {conf_hold_status, wait_hook} = opts;
|
||||||
.catch((err) => this.logger.info({err}, `Error ${prop} participant`));
|
let hookOnly = true;
|
||||||
return true;
|
|
||||||
|
if (this.conf_hold_status !== conf_hold_status) {
|
||||||
|
hookOnly = false;
|
||||||
|
this.conf_hold_status = conf_hold_status;
|
||||||
|
const hold = conf_hold_status === 'hold';
|
||||||
|
|
||||||
|
this.ep.api(`conference ${this.confName} ${hold ? 'mute' : 'unmute'} ${this.memberId}`)
|
||||||
|
.catch((err) => this.logger.info({err}, 'Error muting or unmuting participant'));
|
||||||
|
this.ep.api(`conference ${this.confName} ${hold ? 'deaf' : 'undeaf'} ${this.memberId}`)
|
||||||
|
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hookOnly && this._playSession) {
|
||||||
|
this._playSession.kill();
|
||||||
|
this._playSession = null;
|
||||||
|
}
|
||||||
|
if (wait_hook && this.conf_hold_status === 'hold') {
|
||||||
|
const {dlg} = cs;
|
||||||
|
this._doWaitHookWhileOnHold(cs, dlg, wait_hook);
|
||||||
|
}
|
||||||
|
else if (this.conf_hold_status !== 'hold' && this._playSession) {
|
||||||
|
this._playSession.kill();
|
||||||
|
this._playSession = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deaf(callSid, deafed) {
|
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
|
||||||
if (this.memberId) {
|
do {
|
||||||
const prop = deafed === true ? 'deaf' : 'undeaf';
|
try {
|
||||||
this.ep.api(`conference ${this.confName} ${prop} ${this.memberId}`)
|
const tasks = await this._playHook(cs, dlg, wait_hook);
|
||||||
.catch((err) => this.logger.info({err}, `Error ${prop} participant`));
|
if (0 === tasks.length) break;
|
||||||
return true;
|
} catch (err) {
|
||||||
}
|
if (!this.killed) {
|
||||||
|
this.logger.info(err, `Conference:_doWait: failed retrieving wait_hook for ${this.confName}`);
|
||||||
|
}
|
||||||
|
this._playSession = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (!this.killed && !this.conf_hold_status === 'hold');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -483,6 +515,9 @@ class Conference extends Task {
|
|||||||
dlg,
|
dlg,
|
||||||
ep: cs.ep,
|
ep: cs.ep,
|
||||||
callInfo: cs.callInfo,
|
callInfo: cs.callInfo,
|
||||||
|
accountInfo: cs.accountInfo,
|
||||||
|
memberId: this.memberId,
|
||||||
|
confName: this.confName,
|
||||||
tasks
|
tasks
|
||||||
});
|
});
|
||||||
await this._playSession.exec();
|
await this._playSession.exec();
|
||||||
@@ -503,6 +538,7 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async replaceEndpointAndEnd(cs) {
|
async replaceEndpointAndEnd(cs) {
|
||||||
|
cs.clearConferenceDetails();
|
||||||
if (this.replaced) return;
|
if (this.replaced) return;
|
||||||
this.replaced = true;
|
this.replaced = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
class TaskMessage extends Task {
|
class TaskMessage extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
@@ -8,13 +9,11 @@ class TaskMessage extends Task {
|
|||||||
this.preconditions = TaskPreconditions.None;
|
this.preconditions = TaskPreconditions.None;
|
||||||
|
|
||||||
this.payload = {
|
this.payload = {
|
||||||
message_sid: this.data.message_sid,
|
message_sid: this.data.message_sid || uuidv4(),
|
||||||
provider: this.data.provider,
|
provider: this.data.provider,
|
||||||
to: this.data.to,
|
to: this.data.to,
|
||||||
from: this.data.from,
|
from: this.data.from,
|
||||||
cc: this.data.cc,
|
text: this.data.text
|
||||||
text: this.data.text,
|
|
||||||
media: this.data.media
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -30,7 +29,7 @@ class TaskMessage extends Task {
|
|||||||
let payload = this.payload;
|
let payload = this.payload;
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
try {
|
try {
|
||||||
const {getSBC, getSmpp, dbHelpers} = srf.locals;
|
const {getSmpp, dbHelpers} = srf.locals;
|
||||||
const {lookupSmppGateways} = dbHelpers;
|
const {lookupSmppGateways} = dbHelpers;
|
||||||
|
|
||||||
this.logger.info(`looking up gateways for account_sid: ${accountSid}`);
|
this.logger.info(`looking up gateways for account_sid: ${accountSid}`);
|
||||||
@@ -50,13 +49,14 @@ class TaskMessage extends Task {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
//TMP: smpp only at the moment, need to add http back in
|
||||||
|
/*
|
||||||
this.logger.info({gw, accountSid, provider: this.payload.provider},
|
this.logger.info({gw, accountSid, provider: this.payload.provider},
|
||||||
'Message:exec - no smpp gateways found to send message');
|
'Message:exec - no smpp gateways found to send message');
|
||||||
relativeUrl = 'v1/outboundSMS';
|
relativeUrl = 'v1/outboundSMS';
|
||||||
const sbcAddress = getSBC();
|
const sbcAddress = getSBC();
|
||||||
if (sbcAddress) url = `http://${sbcAddress}:3000/`;
|
if (sbcAddress) url = `http://${sbcAddress}:3000/`;
|
||||||
|
*/
|
||||||
//TMP: smpp only at the moment, need to add http back in
|
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
|
|||||||
@@ -17,8 +17,12 @@ class TaskPlay extends Task {
|
|||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
try {
|
try {
|
||||||
while (!this.killed && this.loop--) {
|
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
||||||
await ep.play(this.url);
|
if (cs.isInConference) {
|
||||||
|
const {memberId, confName, confUuid} = cs;
|
||||||
|
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
|
||||||
|
}
|
||||||
|
else await ep.play(this.url);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
|
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
|
||||||
@@ -30,7 +34,13 @@ class TaskPlay extends Task {
|
|||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
if (this.ep.connected && !this.playComplete) {
|
if (this.ep.connected && !this.playComplete) {
|
||||||
this.logger.debug('TaskPlay:kill - killing audio');
|
this.logger.debug('TaskPlay:kill - killing audio');
|
||||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
if (cs.isInConference) {
|
||||||
|
const {memberId, confName} = cs;
|
||||||
|
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,10 +68,14 @@ class TaskSay extends Task {
|
|||||||
|
|
||||||
this.logger.debug({filepath}, 'synthesized files for tts');
|
this.logger.debug({filepath}, 'synthesized files for tts');
|
||||||
|
|
||||||
while (!this.killed && this.loop-- && this.ep.connected) {
|
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
||||||
let segment = 0;
|
let segment = 0;
|
||||||
do {
|
do {
|
||||||
await ep.play(filepath[segment]);
|
if (cs.isInConference) {
|
||||||
|
const {memberId, confName, confUuid} = cs;
|
||||||
|
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
||||||
|
}
|
||||||
|
else await ep.play(filepath[segment]);
|
||||||
} while (!this.killed && ++segment < filepath.length);
|
} while (!this.killed && ++segment < filepath.length);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -84,7 +88,13 @@ class TaskSay extends Task {
|
|||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
if (this.ep.connected) {
|
if (this.ep.connected) {
|
||||||
this.logger.debug('TaskSay:kill - killing audio');
|
this.logger.debug('TaskSay:kill - killing audio');
|
||||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
if (cs.isInConference) {
|
||||||
|
const {memberId, confName} = cs;
|
||||||
|
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"play": {
|
"play": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"url": "string",
|
"url": "string",
|
||||||
"loop": "number",
|
"loop": "number|string",
|
||||||
"earlyMedia": "boolean"
|
"earlyMedia": "boolean"
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class Task extends Emitter {
|
|||||||
|
|
||||||
this._killInProgress = false;
|
this._killInProgress = false;
|
||||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||||
|
|
||||||
|
/* used when we play a prompt to a member in conference */
|
||||||
|
this._confPlayCompletionPromise = new Promise((resolve) => this._confPlayCompletionResolver = resolve);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +80,21 @@ class Task extends Emitter {
|
|||||||
return this._completionPromise;
|
return this._completionPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* when a play to conference member completes
|
||||||
|
*/
|
||||||
|
notifyConfPlayDone() {
|
||||||
|
this._confPlayCompletionResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* when a subclass task has launched various async activities and is now simply waiting
|
||||||
|
* for them to complete it should call this method to block until that happens
|
||||||
|
*/
|
||||||
|
awaitConfPlayDone() {
|
||||||
|
return this._confPlayCompletionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* provided as a convenience for tasks, this simply calls CallSession#normalizeUrl
|
* provided as a convenience for tasks, this simply calls CallSession#normalizeUrl
|
||||||
*/
|
*/
|
||||||
@@ -118,6 +136,43 @@ class Task extends Emitter {
|
|||||||
cs.replaceApplication(tasks);
|
cs.replaceApplication(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async playToConfMember(ep, memberId, confName, confUuid, filepath) {
|
||||||
|
try {
|
||||||
|
this.logger.debug(`Task:playToConfMember - playing ${filepath} to ${confName}:${memberId}`);
|
||||||
|
|
||||||
|
// listen for conference events
|
||||||
|
const handler = this.__onConferenceEvent.bind(this);
|
||||||
|
ep.conn.on('esl::event::CUSTOM::*', handler) ;
|
||||||
|
const response = await ep.api(`conference ${confName} play ${filepath} ${memberId}`);
|
||||||
|
this.logger.debug({response}, 'Task:playToConfMember - api call returned');
|
||||||
|
await this.awaitConfPlayDone();
|
||||||
|
ep.conn.removeListener('esl::event::CUSTOM::*', handler);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, `Task:playToConfMember - error playing ${filepath} to ${confName}:${memberId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async killPlayToConfMember(ep, memberId, confName) {
|
||||||
|
try {
|
||||||
|
this.logger.debug(`Task:killPlayToConfMember - killing audio to ${confName}:${memberId}`);
|
||||||
|
const response = await ep.api(`conference ${confName} stop ${memberId}`);
|
||||||
|
this.logger.debug({response}, 'Task:killPlayToConfMember - api call returned');
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, `Task:killPlayToConfMember - error killing audio to ${confName}:${memberId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__onConferenceEvent(evt) {
|
||||||
|
const eventName = evt.getHeader('Event-Subclass') ;
|
||||||
|
if (eventName === 'conference::maintenance') {
|
||||||
|
const action = evt.getHeader('Action') ;
|
||||||
|
if (action === 'play-file-member-done') {
|
||||||
|
this.logger.debug('done playing file to conf member');
|
||||||
|
this.notifyConfPlayDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async transferCallToFeatureServer(cs, sipAddress, opts) {
|
async transferCallToFeatureServer(cs, sipAddress, opts) {
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
const {addKey} = cs.srf.locals.dbHelpers;
|
const {addKey} = cs.srf.locals.dbHelpers;
|
||||||
|
|||||||
Reference in New Issue
Block a user