LCC: add conference hold and unhold actions

This commit is contained in:
Dave Horton
2021-09-22 07:39:44 -04:00
parent 3cd4c399d4
commit 862405c232
8 changed files with 173 additions and 39 deletions

View File

@@ -34,7 +34,7 @@ class CallSession extends Emitter {
* @param {array} opts.tasks - tasks we are to execute
* @param {callInfo} opts.callInfo - information about the call
*/
constructor({logger, application, srf, tasks, callInfo, accountInfo}) {
constructor({logger, application, srf, tasks, callInfo, accountInfo, memberId, confName, confUuid}) {
super();
this.logger = logger;
this.application = application;
@@ -42,6 +42,9 @@ class CallSession extends Emitter {
this.callInfo = callInfo;
this.accountInfo = accountInfo;
this.tasks = tasks;
this.memberId = memberId;
this.confName = confName;
this.confUuid = confUuid;
this.taskIdx = 0;
this.stackIdx = 0;
this.callGone = false;
@@ -196,6 +199,27 @@ class CallSession extends Emitter {
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
* @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'));
}
async _lccConfHoldStatus(callSid, hold) {
this.logger.debug(`_lccConfHoldStatus ${hold}`);
async _lccConfHoldStatus(callSid, opts) {
const task = this.currentTask;
if (!task || TaskName.Conference !== task.name) {
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as conference verb is not active');
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
}
// now do the mute/unmute, deaf/undeaf
task.mute(callSid, hold).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
task.deaf(callSid, hold).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
task.doConferenceHold(this, opts);
}
/**
@@ -517,7 +538,7 @@ class CallSession extends Emitter {
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
}
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

View File

@@ -8,7 +8,7 @@ const CallSession = require('./call-session');
*/
class ConfirmCallSession extends CallSession {
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo}) {
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName}) {
super({
logger,
application,
@@ -16,7 +16,9 @@ class ConfirmCallSession extends CallSession {
callSid: dlg.callSid,
tasks,
callInfo,
accountInfo
accountInfo,
memberId,
confName
});
this.dlg = dlg;
this.ep = ep;

View File

@@ -27,7 +27,8 @@ function camelize(str) {
function unhandled(logger, cs, evt) {
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) {
@@ -213,6 +214,7 @@ class Conference extends Task {
this._playSession.kill();
this._playSession = null;
}
cs.clearConferenceDetails();
resolve();
});
@@ -335,6 +337,8 @@ class Conference extends Task {
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
this.memberId = memberId;
this.confUuid = confUuid;
cs.setConferenceDetails(memberId, this.confName, confUuid);
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
this._notifyConferenceEvent(cs, 'join');
@@ -375,22 +379,50 @@ class Conference extends Task {
this.emitter.emit('join', opts);
}
async mute(callSid, muted) {
if (this.memberId) {
const prop = muted === true ? 'mute' : 'unmute';
this.ep.api(`conference ${this.confName} ${prop} ${this.memberId}`)
.catch((err) => this.logger.info({err}, `Error ${prop} participant`));
return true;
async doConferenceHold(cs, opts) {
assert (cs.isInConference);
const {conf_hold_status, wait_hook} = opts;
let hookOnly = 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) {
if (this.memberId) {
const prop = deafed === true ? 'deaf' : 'undeaf';
this.ep.api(`conference ${this.confName} ${prop} ${this.memberId}`)
.catch((err) => this.logger.info({err}, `Error ${prop} participant`));
return true;
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
do {
try {
const tasks = await this._playHook(cs, dlg, wait_hook);
if (0 === tasks.length) break;
} 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,
ep: cs.ep,
callInfo: cs.callInfo,
accountInfo: cs.accountInfo,
memberId: this.memberId,
confName: this.confName,
tasks
});
await this._playSession.exec();
@@ -503,6 +538,7 @@ class Conference extends Task {
}
async replaceEndpointAndEnd(cs) {
cs.clearConferenceDetails();
if (this.replaced) return;
this.replaced = true;
try {

View File

@@ -1,6 +1,7 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const bent = require('bent');
const { v4: uuidv4 } = require('uuid');
class TaskMessage extends Task {
constructor(logger, opts) {
@@ -8,13 +9,11 @@ class TaskMessage extends Task {
this.preconditions = TaskPreconditions.None;
this.payload = {
message_sid: this.data.message_sid,
message_sid: this.data.message_sid || uuidv4(),
provider: this.data.provider,
to: this.data.to,
from: this.data.from,
cc: this.data.cc,
text: this.data.text,
media: this.data.media
text: this.data.text
};
}
@@ -30,7 +29,7 @@ class TaskMessage extends Task {
let payload = this.payload;
await super.exec(cs);
try {
const {getSBC, getSmpp, dbHelpers} = srf.locals;
const {getSmpp, dbHelpers} = srf.locals;
const {lookupSmppGateways} = dbHelpers;
this.logger.info(`looking up gateways for account_sid: ${accountSid}`);
@@ -50,13 +49,14 @@ class TaskMessage extends Task {
};
}
else {
//TMP: smpp only at the moment, need to add http back in
/*
this.logger.info({gw, accountSid, provider: this.payload.provider},
'Message:exec - no smpp gateways found to send message');
relativeUrl = 'v1/outboundSMS';
const sbcAddress = getSBC();
if (sbcAddress) url = `http://${sbcAddress}:3000/`;
//TMP: smpp only at the moment, need to add http back in
*/
return res.sendStatus(404);
}
if (url) {

View File

@@ -17,8 +17,12 @@ class TaskPlay extends Task {
await super.exec(cs);
this.ep = ep;
try {
while (!this.killed && this.loop--) {
await ep.play(this.url);
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
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) {
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
@@ -30,9 +34,15 @@ class TaskPlay extends Task {
super.kill(cs);
if (this.ep.connected && !this.playComplete) {
this.logger.debug('TaskPlay:kill - 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'));
}
}
}
}
module.exports = TaskPlay;

View File

@@ -68,10 +68,14 @@ class TaskSay extends Task {
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;
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);
}
} catch (err) {
@@ -84,9 +88,15 @@ class TaskSay extends Task {
super.kill(cs);
if (this.ep.connected) {
this.logger.debug('TaskSay:kill - 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'));
}
}
}
}
module.exports = TaskSay;

View File

@@ -46,7 +46,7 @@
"play": {
"properties": {
"url": "string",
"loop": "number",
"loop": "number|string",
"earlyMedia": "boolean"
},
"required": [

View File

@@ -23,6 +23,9 @@ class Task extends Emitter {
this._killInProgress = false;
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;
}
/**
* 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
*/
@@ -118,6 +136,43 @@ class Task extends Emitter {
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) {
const uuid = uuidv4();
const {addKey} = cs.srf.locals.dbHelpers;