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 {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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,7 +34,13 @@ class TaskPlay extends Task {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected && !this.playComplete) {
|
||||
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');
|
||||
|
||||
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,7 +88,13 @@ class TaskSay extends Task {
|
||||
super.kill(cs);
|
||||
if (this.ep.connected) {
|
||||
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": {
|
||||
"properties": {
|
||||
"url": "string",
|
||||
"loop": "number",
|
||||
"loop": "number|string",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user