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 {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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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'));
}
} }
} }
} }

View File

@@ -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'));
}
} }
} }
} }

View File

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

View File

@@ -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;