mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
32 Commits
feature/qu
...
v0.6.7-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19dda9398d | ||
|
|
81edf1a6d6 | ||
|
|
72345f83c1 | ||
|
|
bedf25c6a2 | ||
|
|
a9e789f466 | ||
|
|
a779ead79f | ||
|
|
a3d3878218 | ||
|
|
4bc3e03605 | ||
|
|
62106a751f | ||
|
|
4c61ae5fbd | ||
|
|
708c13d5f6 | ||
|
|
7cf342eeb8 | ||
|
|
aebcf2b006 | ||
|
|
f0bd681ccc | ||
|
|
ac263de729 | ||
|
|
862405c232 | ||
|
|
3cd4c399d4 | ||
|
|
0d6cb8a2b3 | ||
|
|
05c5319cbc | ||
|
|
d15fdcf663 | ||
|
|
19f3cbaa43 | ||
|
|
ac8827c885 | ||
|
|
d1d082ceaf | ||
|
|
28415dc750 | ||
|
|
3d0c7fea52 | ||
|
|
3fed15b3b9 | ||
|
|
7c629e6faf | ||
|
|
649b3d5715 | ||
|
|
48fbbd48ad | ||
|
|
dacd3691ed | ||
|
|
df8dac367c | ||
|
|
1a2aaf9845 |
@@ -8,7 +8,7 @@
|
||||
"jsx": false,
|
||||
"modules": false
|
||||
},
|
||||
"ecmaVersion": 2018
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"plugins": ["promise"],
|
||||
"rules": {
|
||||
|
||||
@@ -57,6 +57,11 @@ router.post('/', async(req, res) => {
|
||||
case 'user':
|
||||
uri = `sip:${target.name}`;
|
||||
to = target.name;
|
||||
if (target.overrideTo) {
|
||||
Object.assign(opts.headers, {
|
||||
'X-Override-To': target.overrideTo
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'sip':
|
||||
uri = target.sipUri;
|
||||
@@ -105,8 +110,11 @@ router.post('/', async(req, res) => {
|
||||
|
||||
/* now launch the outdial */
|
||||
try {
|
||||
const dlg = await srf.createUAC(uri, opts, {
|
||||
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
||||
cbRequest: (err, inviteReq) => {
|
||||
/* in case of 302 redirect, this gets called twice, ignore the second */
|
||||
if (res.headersSent) return;
|
||||
|
||||
if (err) {
|
||||
logger.error(err, 'createCall Error creating call');
|
||||
res.status(500).send('Call Failure');
|
||||
|
||||
@@ -45,7 +45,7 @@ module.exports = function(srf, logger) {
|
||||
// TODO: alert
|
||||
return res.send(503, {headers: {'X-Reason': 'Account exists but is inactive'}});
|
||||
}
|
||||
logger.debug({accountInfo: req.locals.accountInfo}, `retrieved account info for ${account_sid}`);
|
||||
logger.debug({accountInfo: req.locals?.accountInfo?.account}, `retrieved account info for ${account_sid}`);
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info({err}, `Error retrieving account details for account ${account_sid}`);
|
||||
@@ -167,7 +167,7 @@ module.exports = function(srf, logger) {
|
||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.info(`Error retrieving or parsing application: ${err.message}`);
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err.message}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ const CallSession = require('./call-session');
|
||||
|
||||
*/
|
||||
class AdultingCallSession extends CallSession {
|
||||
constructor({logger, application, singleDialer, tasks, callInfo}) {
|
||||
constructor({logger, application, singleDialer, tasks, callInfo, accountInfo}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
srf: singleDialer.dlg.srf,
|
||||
tasks,
|
||||
callInfo
|
||||
callInfo,
|
||||
accountInfo
|
||||
});
|
||||
this.sd = singleDialer;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -192,13 +195,37 @@ class CallSession extends Emitter {
|
||||
return this.constructor.name === 'SmsCallSession';
|
||||
}
|
||||
|
||||
get 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
|
||||
* @param {*} vendor - google or aws
|
||||
*/
|
||||
getSpeechCredentials(vendor, type) {
|
||||
const {writeAlerts, AlertType} = this.srf.locals;
|
||||
this.logger.debug({vendor, type, speech: this.accountInfo.speech}, `searching for speech for vendor ${vendor}`);
|
||||
if (this.accountInfo.speech && this.accountInfo.speech.length > 0) {
|
||||
const credential = this.accountInfo.speech.find((s) => s.vendor === vendor);
|
||||
if (credential && (
|
||||
@@ -336,7 +363,7 @@ class CallSession extends Emitter {
|
||||
*/
|
||||
_lccCallStatus(opts) {
|
||||
if (opts.call_status === CallStatus.Completed && this.dlg) {
|
||||
this.logger.info('CallSession:updateCall hanging up call due to request from api');
|
||||
this.logger.info('CallSession:_lccCallStatus hanging up call due to request from api');
|
||||
this._callerHungup();
|
||||
}
|
||||
else if (opts.call_status === CallStatus.NoAnswer) {
|
||||
@@ -364,13 +391,13 @@ class CallSession extends Emitter {
|
||||
async _lccCallHook(opts) {
|
||||
const webhooks = [];
|
||||
let sd;
|
||||
if (opts.call_hook) webhooks.push(this.requestor.request(opts.call_hook, this.callInfo));
|
||||
if (opts.call_hook) webhooks.push(this.requestor.request(opts.call_hook, this.callInfo.toJSON()));
|
||||
if (opts.child_call_hook) {
|
||||
/* child call hook only allowed from a connected Dial state */
|
||||
const task = this.currentTask;
|
||||
sd = task.sd;
|
||||
if (task && TaskName.Dial === task.name && sd) {
|
||||
webhooks.push(this.requestor.request(opts.child_call_hook, sd.callInfo));
|
||||
webhooks.push(this.requestor.request(opts.child_call_hook, sd.callInfo.toJSON()));
|
||||
}
|
||||
}
|
||||
const [tasks1, tasks2] = await Promise.all(webhooks);
|
||||
@@ -385,7 +412,7 @@ class CallSession extends Emitter {
|
||||
const {parentLogger} = this.srf.locals;
|
||||
const childLogger = parentLogger.child({callId: this.callId, callSid: sd.callSid});
|
||||
const t = normalizeJambones(childLogger, childTasks).map((tdata) => makeTask(childLogger, tdata));
|
||||
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list for child call');
|
||||
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list for child call');
|
||||
const cs = await sd.doAdulting({
|
||||
logger: childLogger,
|
||||
application: this.application,
|
||||
@@ -397,7 +424,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
if (tasks) {
|
||||
const t = normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata));
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list');
|
||||
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list');
|
||||
this.replaceApplication(t);
|
||||
}
|
||||
else {
|
||||
@@ -414,23 +441,39 @@ class CallSession extends Emitter {
|
||||
async _lccListenStatus(opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || ![TaskName.Dial, TaskName.Listen].includes(task.name)) {
|
||||
return this.logger.info(`CallSession:updateCall - invalid listen_status in task ${task.name}`);
|
||||
return this.logger.info(`CallSession:_lccListenStatus - invalid listen_status in task ${task.name}`);
|
||||
}
|
||||
const listenTask = task.name === TaskName.Listen ? task : task.listenTask;
|
||||
if (!listenTask) {
|
||||
return this.logger.info('CallSession:updateCall - invalid listen_status: Dial does not have a listen');
|
||||
return this.logger.info('CallSession:_lccListenStatus - invalid listen_status: Dial does not have a listen');
|
||||
}
|
||||
listenTask.updateListen(opts.listen_status);
|
||||
}
|
||||
|
||||
async _lccMuteStatus(callSid, mute) {
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
// this whole thing requires us to be in a Dial or Conference verb
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Dial !== task.name) {
|
||||
return this.logger.info('CallSession:_lccMute - invalid command as dial is not active');
|
||||
if (!task || ![TaskName.Dial, TaskName.Conference].includes(task.name)) {
|
||||
return this.logger.info('CallSession:_lccMuteStatus - invalid: neither dial nor conference are not active');
|
||||
}
|
||||
// now do the whisper
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
|
||||
// now do the mute/unmute
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMuteStatus'));
|
||||
}
|
||||
|
||||
async _lccConfHoldStatus(callSid, opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
|
||||
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
|
||||
}
|
||||
task.doConferenceHold(this, opts);
|
||||
}
|
||||
|
||||
async _lccConfMuteStatus(callSid, opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
|
||||
return this.logger.info('CallSession:_lccConfHoldStatus - invalid command as call is not in conference');
|
||||
}
|
||||
task.doConferenceMuteNonModerators(this, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,7 +493,7 @@ class CallSession extends Emitter {
|
||||
// allow user to provide a url object, a url string, an array of tasks, or a single task
|
||||
if (typeof whisper === 'string' || (typeof whisper === 'object' && whisper.url)) {
|
||||
// retrieve a url
|
||||
const json = await this.requestor(opts.call_hook, this.callInfo);
|
||||
const json = await this.requestor(opts.call_hook, this.callInfo.toJSON());
|
||||
tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
}
|
||||
else if (Array.isArray(whisper)) {
|
||||
@@ -481,20 +524,6 @@ class CallSession extends Emitter {
|
||||
task.whisper(tasks, callSid).catch((err) => this.logger.error(err, 'CallSession:_lccWhisper'));
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- mute or unmute an endpoint
|
||||
* @param {array} opts - array of play or say tasks
|
||||
*/
|
||||
async _lccMute(callSid, mute) {
|
||||
|
||||
// this whole thing requires us to be in a Dial verb
|
||||
const task = this.currentTask;
|
||||
if (!task || TaskName.Dial !== task.name) {
|
||||
return this.logger.info('CallSession:_lccMute - not possible since we are not in a dial');
|
||||
}
|
||||
|
||||
task.mute(callSid, mute).catch((err) => this.logger.error(err, 'CallSession:_lccMute'));
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control
|
||||
@@ -516,6 +545,12 @@ class CallSession extends Emitter {
|
||||
else if (opts.mute_status) {
|
||||
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
|
||||
}
|
||||
else if (opts.conf_hold_status) {
|
||||
await this._lccConfHoldStatus(callSid, opts);
|
||||
}
|
||||
else if (opts.conf_mute_status) {
|
||||
await this._lccConfMuteStatus(callSid, opts);
|
||||
}
|
||||
|
||||
// whisper may be the only thing we are asked to do, or it may that
|
||||
// we are doing a whisper after having muted, paused reccording etc..
|
||||
@@ -784,7 +819,8 @@ class CallSession extends Emitter {
|
||||
}
|
||||
else {
|
||||
this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found');
|
||||
this.queueEventHookRequestor = new Requestor(this.logger, r[0]);
|
||||
this.queueEventHookRequestor = new Requestor(this.logger, this.accountSid,
|
||||
r[0], this.webhook_secret);
|
||||
this.queueEventHook = r[0];
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -890,6 +926,28 @@ class CallSession extends Emitter {
|
||||
};
|
||||
}
|
||||
|
||||
async releaseMediaToSBC(remoteSdp) {
|
||||
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
|
||||
await this.dlg.modify(remoteSdp, {
|
||||
headers: {
|
||||
'X-Reason': 'release-media'
|
||||
}
|
||||
});
|
||||
this.ep.destroy()
|
||||
.then(() => this.ep = null)
|
||||
.catch((err) => this.logger.error({err}, 'CallSession:releaseMediaToSBC: Error destroying endpoint'));
|
||||
}
|
||||
|
||||
async reAnchorMedia() {
|
||||
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
await this.dlg.modify(this.ep.local.sdp, {
|
||||
headers: {
|
||||
'X-Reason': 'anchor-media'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called any time call status changes. This method both invokes the
|
||||
* call_status_hook callback as well as updates the realtime database
|
||||
@@ -909,14 +967,14 @@ class CallSession extends Emitter {
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
try {
|
||||
this.notifier.request(this.call_status_hook, this.callInfo);
|
||||
this.notifier.request(this.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||
}
|
||||
|
||||
// update calls db
|
||||
//this.logger.debug(`updating redis with ${JSON.stringify(this.callInfo)}`);
|
||||
this.updateCallStatus(Object.assign({}, this.callInfo), this.serviceUrl)
|
||||
this.updateCallStatus(Object.assign({}, this.callInfo.toJSON()), this.serviceUrl)
|
||||
.catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,17 @@ const CallSession = require('./call-session');
|
||||
|
||||
*/
|
||||
class ConfirmCallSession extends CallSession {
|
||||
constructor({logger, application, dlg, ep, tasks, callInfo}) {
|
||||
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName}) {
|
||||
super({
|
||||
logger,
|
||||
application,
|
||||
srf: dlg.srf,
|
||||
callSid: dlg.callSid,
|
||||
tasks,
|
||||
callInfo
|
||||
callInfo,
|
||||
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) {
|
||||
@@ -45,7 +46,7 @@ class Conference extends Task {
|
||||
|
||||
this.confName = this.data.name;
|
||||
[
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit',
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
|
||||
].forEach((attr) => this[attr] = this.data[attr]);
|
||||
|
||||
@@ -213,6 +214,7 @@ class Conference extends Task {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
cs.clearConferenceDetails();
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -330,11 +332,16 @@ class Conference extends Task {
|
||||
|
||||
const opts = {};
|
||||
if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
|
||||
if (this.startConferenceOnEnter) Object.assign(opts, {flags: {moderator: true}});
|
||||
if (this.joinMuted) Object.assign(opts, {flags: {mute: true}});
|
||||
|
||||
try {
|
||||
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
|
||||
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');
|
||||
@@ -371,9 +378,70 @@ class Conference extends Task {
|
||||
*/
|
||||
notifyStartConference(cs, opts) {
|
||||
this.logger.info({opts}, `Conference:notifyStartConference: conference ${this.confName} has now started`);
|
||||
this.conferenceStartTime = new Date();
|
||||
this.emitter.emit('join', opts);
|
||||
}
|
||||
|
||||
async doConferenceMuteNonModerators(cs, opts) {
|
||||
const mute = opts.conf_mute_status === 'mute';
|
||||
assert (cs.isInConference);
|
||||
|
||||
this.logger.info(`Conference:doConferenceMuteNonModerators ${mute ? 'muting' : 'unmuting'} non-moderators`);
|
||||
this.ep.api(`conference ${this.confName} ${mute ? 'mute' : 'unmute'} non_moderator`)
|
||||
.catch((err) => this.logger.info({err}, 'Error muting or unmuting non_moderators'));
|
||||
|
||||
if (this.conf_hold_status !== 'hold' && this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
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 _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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ourselves to the waitlist of sessions to be notified once
|
||||
* the conference starts
|
||||
@@ -464,6 +532,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();
|
||||
@@ -484,6 +555,7 @@ class Conference extends Task {
|
||||
}
|
||||
|
||||
async replaceEndpointAndEnd(cs) {
|
||||
cs.clearConferenceDetails();
|
||||
if (this.replaced) return;
|
||||
this.replaced = true;
|
||||
try {
|
||||
|
||||
@@ -130,6 +130,10 @@ class TaskDial extends Task {
|
||||
|
||||
get name() { return TaskName.Dial; }
|
||||
|
||||
get canReleaseMedia() {
|
||||
return !process.env.ANCHOR_MEDIA_ALWAYS && !this.listenTask && !this.transcribeTask;
|
||||
}
|
||||
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
try {
|
||||
@@ -142,13 +146,12 @@ class TaskDial extends Task {
|
||||
this.epOther.play(this.dialMusic).catch((err) => {});
|
||||
}
|
||||
}
|
||||
if (this.epOther) this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
|
||||
await this._attemptCalls(cs);
|
||||
await this.awaitTaskDone();
|
||||
this.logger.debug({callSid: this.cs.callSid}, 'Dial:exec task is done, sending actionHook if any');
|
||||
await this.performAction(this.results, this.killReason !== KillReason.Replaced);
|
||||
this._removeDtmfDetection(cs, this.epOther);
|
||||
this._removeDtmfDetection(cs, this.ep);
|
||||
this._removeDtmfDetection(cs.dlg);
|
||||
this._removeDtmfDetection(this.dlg);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskDial:exec terminating with error');
|
||||
this.kill(cs);
|
||||
@@ -158,8 +161,8 @@ class TaskDial extends Task {
|
||||
async kill(cs, reason) {
|
||||
super.kill(cs);
|
||||
this.killReason = reason || KillReason.Hangup;
|
||||
this._removeDtmfDetection(this.cs, this.epOther);
|
||||
this._removeDtmfDetection(this.cs, this.ep);
|
||||
this._removeDtmfDetection(cs.dlg);
|
||||
this._removeDtmfDetection(this.dlg);
|
||||
this._killOutdials();
|
||||
if (this.sd) {
|
||||
this.sd.kill();
|
||||
@@ -177,9 +180,14 @@ class TaskDial extends Task {
|
||||
* @param {*} tasks - array of play/say tasks to execute
|
||||
*/
|
||||
async whisper(tasks, callSid) {
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
|
||||
try {
|
||||
const cs = this.callSession;
|
||||
if (!this.ep && !this.epOther) {
|
||||
await this.reAnchorMedia(this.callSession, this.sd);
|
||||
}
|
||||
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
|
||||
|
||||
this.logger.debug('Dial:whisper unbridging endpoints');
|
||||
await this.epOther.unbridge();
|
||||
this.logger.debug('Dial:whisper executing tasks');
|
||||
@@ -188,7 +196,12 @@ class TaskDial extends Task {
|
||||
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
|
||||
}
|
||||
this.logger.debug('Dial:whisper tasks complete');
|
||||
if (!cs.callGone && this.epOther) this.epOther.bridge(this.ep);
|
||||
if (!cs.callGone && this.epOther) {
|
||||
|
||||
/* if we can release the media back to the SBC, do so now */
|
||||
if (this.canReleaseMedia) this._releaseMedia(cs, this.sd);
|
||||
else this.epOther.bridge(this.ep);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Dial:whisper error');
|
||||
}
|
||||
@@ -198,14 +211,19 @@ class TaskDial extends Task {
|
||||
* mute or unmute one side of the call
|
||||
*/
|
||||
async mute(callSid, doMute) {
|
||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:mute: no paired endpoint found');
|
||||
const parentCall = callSid !== this.callSid;
|
||||
const dlg = parentCall ? this.callSession.dlg : this.dlg;
|
||||
const hdr = `${doMute ? 'mute' : 'unmute'} call leg`;
|
||||
try {
|
||||
const parentCall = callSid !== this.callSid;
|
||||
const ep = parentCall ? this.epOther : this.ep;
|
||||
await ep[doMute ? 'mute' : 'unmute']();
|
||||
this.logger.debug(`Dial:mute ${doMute ? 'muted' : 'unmuted'} ${parentCall ? 'parentCall' : 'childCall'}`);
|
||||
/* let rtpengine do the mute / unmute */
|
||||
await dlg.request({
|
||||
method: 'INFO',
|
||||
headers: {
|
||||
'X-Reason': hdr
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Dial:mute error');
|
||||
this.logger.info({err}, `Dial:mute - ${hdr} error`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,35 +235,39 @@ class TaskDial extends Task {
|
||||
this.dials.clear();
|
||||
}
|
||||
|
||||
_installDtmfDetection(cs, ep, dtmfDetector) {
|
||||
if (ep && this.dtmfHook && !ep.dtmfDetector) {
|
||||
ep.dtmfDetector = dtmfDetector;
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
_installDtmfDetection(cs, dlg) {
|
||||
dlg.on('info', this._onInfo.bind(this, cs, dlg));
|
||||
}
|
||||
_removeDtmfDetection(cs, ep) {
|
||||
if (ep) {
|
||||
delete ep.dtmfDetector;
|
||||
ep.removeAllListeners('dtmf');
|
||||
}
|
||||
_removeDtmfDetection(dlg) {
|
||||
dlg && dlg.removeAllListeners('info');
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
if (ep.dtmfDetector) {
|
||||
const match = ep.dtmfDetector.keyPress(evt.dtmf);
|
||||
if (match) {
|
||||
this.logger.debug({callSid: this.cs.callSid}, `Dial:_onDtmf triggered dtmf match: ${match}`);
|
||||
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
|
||||
cs.requestor :
|
||||
(this.sd ? this.sd.requestor : null);
|
||||
if (!requestor) {
|
||||
this.logger.info(`Dial:_onDtmf got digits on B leg after adulting: ${evt.dtmf}`);
|
||||
}
|
||||
else {
|
||||
requestor.request(this.dtmfHook, {dtmf: match, ...cs.callInfo})
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
_onInfo(cs, dlg, req, res) {
|
||||
res.send(200);
|
||||
if (req.get('Content-Type') !== 'application/dtmf-relay') return;
|
||||
|
||||
const dtmfDetector = dlg === cs.dlg ? this.parentDtmfCollector : this.childDtmfCollector;
|
||||
if (!dtmfDetector) return;
|
||||
let requestor, callSid, callInfo;
|
||||
if (dtmfDetector === this.parentDtmfCollector) {
|
||||
requestor = cs.requestor;
|
||||
callSid = cs.callSid;
|
||||
callInfo = cs.callInfo;
|
||||
}
|
||||
else {
|
||||
requestor = this.sd?.requestor;
|
||||
callSid = this.sd?.callSid;
|
||||
callInfo = this.sd?.callInfo;
|
||||
}
|
||||
if (!requestor) return;
|
||||
const arr = /Signal=([0-9#*])/.exec(req.body);
|
||||
if (!arr) return;
|
||||
const key = arr[1];
|
||||
const match = dtmfDetector.keyPress(key);
|
||||
if (match) {
|
||||
this.logger.info({callSid}, `Dial:_onInfo triggered dtmf match: ${match}`);
|
||||
requestor.request(this.dtmfHook, {dtmf: match, ...callInfo.toJSON()})
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +277,7 @@ class TaskDial extends Task {
|
||||
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
|
||||
|
||||
/* send outbound legs back to the same SBC (to support static IP feature) */
|
||||
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port};transport=tcp`;
|
||||
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port}`;
|
||||
|
||||
if (this.dialMusic) {
|
||||
// play dial music to caller while we outdial
|
||||
@@ -322,7 +344,8 @@ class TaskDial extends Task {
|
||||
sbcAddress,
|
||||
target: t,
|
||||
opts,
|
||||
callInfo: cs.callInfo
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo
|
||||
});
|
||||
this.dials.set(sd.callSid, sd);
|
||||
|
||||
@@ -366,9 +389,13 @@ class TaskDial extends Task {
|
||||
break;
|
||||
}
|
||||
})
|
||||
.on('accept', () => {
|
||||
.on('accept', async() => {
|
||||
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
||||
this._connectSingleDial(cs, sd);
|
||||
try {
|
||||
await this._connectSingleDial(cs, sd);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial ');
|
||||
}
|
||||
})
|
||||
.on('decline', () => {
|
||||
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
|
||||
@@ -394,8 +421,8 @@ class TaskDial extends Task {
|
||||
});
|
||||
}
|
||||
|
||||
_connectSingleDial(cs, sd) {
|
||||
if (!this.bridged) {
|
||||
async _connectSingleDial(cs, sd) {
|
||||
if (!this.bridged && !this.canReleaseMedia) {
|
||||
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
|
||||
if (this.epOther) {
|
||||
this.epOther.api('uuid_break', this.epOther.uuid);
|
||||
@@ -405,7 +432,7 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
// ding! ding! ding! we have a winner
|
||||
this._selectSingleDial(cs, sd);
|
||||
await this._selectSingleDial(cs, sd);
|
||||
this._killOutdials(); // NB: order is important
|
||||
}
|
||||
|
||||
@@ -418,7 +445,7 @@ class TaskDial extends Task {
|
||||
* - launch any nested tasks
|
||||
* - and establish a handler to clean up if the called party hangs up
|
||||
*/
|
||||
_selectSingleDial(cs, sd) {
|
||||
async _selectSingleDial(cs, sd) {
|
||||
debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`);
|
||||
this.dials.delete(sd.callSid);
|
||||
|
||||
@@ -426,7 +453,7 @@ class TaskDial extends Task {
|
||||
this.callSid = sd.callSid;
|
||||
if (this.earlyMedia) {
|
||||
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
||||
cs.propagateAnswer();
|
||||
await cs.propagateAnswer();
|
||||
}
|
||||
if (this.timeLimit) {
|
||||
this.timerMaxCallDuration = setTimeout(() => {
|
||||
@@ -442,7 +469,7 @@ class TaskDial extends Task {
|
||||
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
|
||||
sessionTracker.remove(this.callSid);
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.ep.unbridge();
|
||||
this.ep && this.ep.unbridge();
|
||||
this.kill(cs);
|
||||
}
|
||||
});
|
||||
@@ -453,10 +480,14 @@ class TaskDial extends Task {
|
||||
dialCallSid: sd.callSid,
|
||||
});
|
||||
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.ep, this.childDtmfCollector);
|
||||
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
||||
|
||||
if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep);
|
||||
if (this.listenTask) this.listenTask.exec(cs, this.ep);
|
||||
|
||||
/* if we can release the media back to the SBC, do so now */
|
||||
if (this.canReleaseMedia) this._releaseMedia(cs, sd);
|
||||
}
|
||||
|
||||
_bridgeEarlyMedia(sd) {
|
||||
@@ -468,6 +499,33 @@ class TaskDial extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the media from freeswitch
|
||||
* @param {*} cs
|
||||
* @param {*} sd
|
||||
*/
|
||||
async _releaseMedia(cs, sd) {
|
||||
assert(cs.ep && sd.ep);
|
||||
|
||||
try {
|
||||
this.logger.info('Dial:_releaseMedia - releasing media from freewitch');
|
||||
const aLegSdp = cs.ep.remote.sdp;
|
||||
const bLegSdp = sd.ep.remote.sdp;
|
||||
await Promise.all[sd.releaseMediaToSBC(aLegSdp), cs.releaseMediaToSBC(bLegSdp)];
|
||||
this.epOther = null;
|
||||
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Dial:_releaseMedia error');
|
||||
}
|
||||
}
|
||||
|
||||
async reAnchorMedia(cs, sd) {
|
||||
if (cs.ep && sd.ep) return;
|
||||
|
||||
this.logger.info('Dial:reAnchorMedia - re-anchoring media to freewitch');
|
||||
await Promise.all([sd.reAnchorMedia(), cs.reAnchorMedia()]);
|
||||
this.epOther = cs.ep;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskDial;
|
||||
|
||||
@@ -9,10 +9,22 @@ class Dialogflow extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.credentials = this.data.credentials;
|
||||
if (this.data.environment) this.project = `${this.data.project}:${this.data.environment}`;
|
||||
else this.project = this.data.project;
|
||||
|
||||
/* set project id with environment and region (optionally) */
|
||||
if (this.data.environment && this.data.region) {
|
||||
this.project = `${this.data.project}:${this.data.environment}:${this.data.region}`;
|
||||
}
|
||||
else if (this.data.environment) {
|
||||
this.project = `${this.data.project}:${this.data.environment}`;
|
||||
}
|
||||
else if (this.data.region) {
|
||||
this.project = `${this.data.project}::${this.data.region}`;
|
||||
}
|
||||
else {
|
||||
this.project = this.data.project;
|
||||
}
|
||||
|
||||
this.lang = this.data.lang || 'en-US';
|
||||
this.welcomeEvent = this.data.welcomeEvent || '';
|
||||
if (this.welcomeEvent.length && this.data.welcomeEventParams && typeof this.data.welcomeEventParams === 'object') {
|
||||
@@ -198,6 +210,7 @@ class Dialogflow extends Task {
|
||||
/* if we are using tts and a message was provided, play it out */
|
||||
if (this.vendor && intent.fulfillmentText && intent.fulfillmentText.length > 0) {
|
||||
const {srf} = cs;
|
||||
const {stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
this.waitingForPlayStart = false;
|
||||
|
||||
@@ -217,7 +230,7 @@ class Dialogflow extends Task {
|
||||
credentials: this.ttsCredentials
|
||||
};
|
||||
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
|
||||
const {filePath, servedFromCache} = await synthAudio(obj);
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, obj);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
|
||||
|
||||
|
||||
@@ -79,12 +79,14 @@ class TaskEnqueue extends Task {
|
||||
this.notifyUrl = url;
|
||||
|
||||
/* invoke account-level webhook for queue event notifications */
|
||||
cs.performQueueWebhook({
|
||||
event: 'join',
|
||||
queue: this.data.name,
|
||||
length: members,
|
||||
joinTime: this.waitStartTime
|
||||
});
|
||||
try {
|
||||
cs.performQueueWebhook({
|
||||
event: 'join',
|
||||
queue: this.data.name,
|
||||
length: members,
|
||||
joinTime: this.waitStartTime
|
||||
});
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
async _removeFromQueue(cs) {
|
||||
@@ -114,6 +116,7 @@ class TaskEnqueue extends Task {
|
||||
this.bridgeDetails = opts;
|
||||
this.logger.info({bridgeDetails: this.bridgeDetails}, `time to dequeue from ${this.queueName}`);
|
||||
if (this._playSession) {
|
||||
this._leave = false;
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
@@ -233,6 +236,7 @@ class TaskEnqueue extends Task {
|
||||
});
|
||||
|
||||
// resolve when either side hangs up
|
||||
this.state = QueueResults.Bridged;
|
||||
this.emitter
|
||||
.on('hangup', () => {
|
||||
this.logger.info('TaskEnqueue:_bridgeLocal ending with hangup from dequeue party');
|
||||
@@ -325,10 +329,9 @@ class TaskEnqueue extends Task {
|
||||
|
||||
// check for 'leave' verb and only execute tasks up till then
|
||||
const tasksToRun = [];
|
||||
let leave = false;
|
||||
for (const o of tasks) {
|
||||
if (o.name === TaskName.Leave) {
|
||||
leave = true;
|
||||
this._leave = true;
|
||||
this.logger.info('waitHook returned a leave task');
|
||||
break;
|
||||
}
|
||||
@@ -343,12 +346,13 @@ class TaskEnqueue extends Task {
|
||||
dlg,
|
||||
ep: cs.ep,
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo,
|
||||
tasks: tasksToRun
|
||||
});
|
||||
await this._playSession.exec();
|
||||
this._playSession = null;
|
||||
}
|
||||
if (leave) {
|
||||
if (this._leave) {
|
||||
this.state = QueueResults.Leave;
|
||||
this.kill(cs);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
|
||||
class TaskGather extends Task {
|
||||
constructor(logger, opts) {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
@@ -40,6 +40,8 @@ class TaskGather extends Task {
|
||||
|
||||
if (this.say) this.sayTask = makeTask(this.logger, {say: this.say}, this);
|
||||
if (this.play) this.playTask = makeTask(this.logger, {play: this.play}, this);
|
||||
|
||||
this.parentTask = parentTask;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Gather; }
|
||||
@@ -92,7 +94,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
if (this.input.includes('digits')) {
|
||||
ep.on('dtmf', this._onDtmf.bind(this, ep));
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
|
||||
await this.awaitTaskDone();
|
||||
@@ -106,19 +108,19 @@ class TaskGather extends Task {
|
||||
|
||||
kill(cs) {
|
||||
super.kill(cs);
|
||||
this._killAudio();
|
||||
this._killAudio(cs);
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
this._resolve('killed');
|
||||
}
|
||||
|
||||
_onDtmf(ep, evt) {
|
||||
_onDtmf(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
||||
else {
|
||||
this.digitBuffer += evt.dtmf;
|
||||
if (this.digitBuffer.length === this.numDigits) this._resolve('dtmf-num-digits');
|
||||
}
|
||||
this._killAudio();
|
||||
this._killAudio(cs);
|
||||
}
|
||||
|
||||
async _initSpeech(cs, ep) {
|
||||
@@ -154,7 +156,6 @@ class TaskGather extends Task {
|
||||
});
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
}
|
||||
this.logger.debug({vars: opts}, 'setting freeswitch vars');
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
|
||||
@@ -190,14 +191,16 @@ class TaskGather extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_killAudio() {
|
||||
_killAudio(cs) {
|
||||
if (this.sayTask && !this.sayTask.killed) {
|
||||
this.sayTask.removeAllListeners('playDone');
|
||||
this.sayTask.kill();
|
||||
this.sayTask.kill(cs);
|
||||
this.sayTask = null;
|
||||
}
|
||||
if (this.playTask && !this.playTask.killed) {
|
||||
this.playTask.removeAllListeners('playDone');
|
||||
this.playTask.kill();
|
||||
this.playTask.kill(cs);
|
||||
this.playTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,10 +235,15 @@ class TaskGather extends Task {
|
||||
|
||||
this._clearTimer();
|
||||
if (reason.startsWith('dtmf')) {
|
||||
await this.performAction({digits: this.digitBuffer});
|
||||
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
}
|
||||
else if (reason.startsWith('speech')) {
|
||||
await this.performAction({speech: evt});
|
||||
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
||||
else await this.performAction({speech: evt, reason: 'speechDetected'});
|
||||
}
|
||||
else if (reason.startsWith('timeout')) {
|
||||
if (this.parentTask) this.parentTask.emit('timeout', evt);
|
||||
else await this.performAction({reason: 'timeout'});
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -182,12 +182,13 @@ class Lex extends Task {
|
||||
const type = messages[0].type;
|
||||
if (['PlainText', 'SSML'].includes(type) && msg) {
|
||||
const {srf} = cs;
|
||||
const {stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
|
||||
try {
|
||||
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {filePath, servedFromCache} = await synthAudio({
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
text: msg,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
|
||||
@@ -122,6 +122,11 @@ class TaskListen extends Task {
|
||||
if (this.finishOnKey || this.passDtmf) {
|
||||
ep.on('dtmf', this._dtmfHandler);
|
||||
}
|
||||
|
||||
/* support bi-directional audio */
|
||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
|
||||
}
|
||||
|
||||
_removeListeners(ep) {
|
||||
@@ -154,6 +159,29 @@ class TaskListen extends Task {
|
||||
this.logger.info(evt, 'TaskListen:_onConnectFailure');
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onPlayAudio(ep, evt) {
|
||||
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
|
||||
try {
|
||||
const results = await ep.play(evt.file);
|
||||
this.logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
||||
ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.error({err}, 'Error playing file');
|
||||
}
|
||||
}
|
||||
|
||||
_onKillAudio(ep) {
|
||||
this.logger.info('received kill_audio event');
|
||||
ep.api('uuid_break', ep.uuid);
|
||||
}
|
||||
|
||||
_onDisconnect(ep, cs) {
|
||||
this.logger.debug('_onDisconnect: TaskListen terminating task');
|
||||
this.kill(cs);
|
||||
}
|
||||
|
||||
_onError(ep, evt) {
|
||||
this.logger.info(evt, 'TaskListen:_onError');
|
||||
this.notifyTaskDone();
|
||||
|
||||
@@ -48,6 +48,9 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.Message:
|
||||
const TaskMessage = require('./message');
|
||||
return new TaskMessage(logger, data, parent);
|
||||
case TaskName.Rasa:
|
||||
const TaskRasa = require('./rasa');
|
||||
return new TaskRasa(logger, data, parent);
|
||||
case TaskName.Say:
|
||||
const TaskSay = require('./say');
|
||||
return new TaskSay(logger, data, parent);
|
||||
|
||||
@@ -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,
|
||||
provider: this.data.provider,
|
||||
message_sid: this.data.message_sid || uuidv4(),
|
||||
carrier: this.data.carrier,
|
||||
to: this.data.to,
|
||||
from: this.data.from,
|
||||
cc: this.data.cc,
|
||||
text: this.data.text,
|
||||
media: this.data.media
|
||||
text: this.data.text
|
||||
};
|
||||
|
||||
}
|
||||
@@ -28,16 +27,18 @@ class TaskMessage extends Task {
|
||||
const {srf, accountSid} = cs;
|
||||
const {res} = cs.callInfo;
|
||||
let payload = this.payload;
|
||||
const actionParams = {message_sid: this.payload.message_sid};
|
||||
|
||||
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}`);
|
||||
this.logger.debug(`looking up gateways for account_sid: ${accountSid}`);
|
||||
const r = await lookupSmppGateways(accountSid);
|
||||
let gw, url, relativeUrl;
|
||||
if (r.length > 0) {
|
||||
gw = r.find((o) => 1 === o.sg.outbound && (!this.payload.provider || o.vc.name === this.payload.provider));
|
||||
gw = r.find((o) => 1 === o.sg.outbound && (!this.payload.carrier || o.vc.name === this.payload.carrier));
|
||||
}
|
||||
if (gw) {
|
||||
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
|
||||
@@ -50,37 +51,75 @@ class TaskMessage extends Task {
|
||||
};
|
||||
}
|
||||
else {
|
||||
this.logger.info({gw, accountSid, provider: this.payload.provider},
|
||||
//TMP: smpp only at the moment, need to add http back in
|
||||
/*
|
||||
this.logger.info({gw, accountSid, carrier: this.payload.carrier},
|
||||
'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);
|
||||
*/
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
message_status: 'no carriers'
|
||||
}).catch((err) => {});
|
||||
if (res) res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
if (url) {
|
||||
const post = bent(url, 'POST', 'json', 201);
|
||||
const post = bent(url, 'POST', 'json', 201, 480);
|
||||
this.logger.info({payload, url}, 'Message:exec sending outbound SMS');
|
||||
const response = await post(relativeUrl, payload);
|
||||
this.logger.info({response}, 'Successfully sent SMS');
|
||||
if (cs.callInfo.res) {
|
||||
this.logger.info('Message:exec sending 200 OK response to HTTP POST from api server');
|
||||
res.status(200).json({
|
||||
sid: cs.callInfo.messageSid,
|
||||
providerResponse: response
|
||||
});
|
||||
const {smpp_err_code, carrier, message_id, message} = response;
|
||||
if (smpp_err_code) {
|
||||
this.logger.info({response}, 'SMPP error sending SMS');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
carrier,
|
||||
carrier_message_id: message_id,
|
||||
message_status: 'failure',
|
||||
message_failure_reason: message
|
||||
}).catch((err) => {});
|
||||
if (res) {
|
||||
res.status(480).json({
|
||||
...response,
|
||||
sid: cs.callInfo.messageSid
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const {message_id, carrier} = response;
|
||||
this.logger.info({response}, 'Successfully sent SMS');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
carrier,
|
||||
carrier_message_id: message_id,
|
||||
message_status: 'success',
|
||||
}).catch((err) => {});
|
||||
if (res) {
|
||||
res.status(200).json({
|
||||
sid: cs.callInfo.messageSid,
|
||||
carrierResponse: response
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: action Hook
|
||||
}
|
||||
else {
|
||||
this.logger.info('Message:exec - unable to send SMS as there are no available SMS gateways');
|
||||
res.status(422).json({message: 'no configured SMS gateways'});
|
||||
this.logger.info('Message:exec - unable to send SMS as SMPP is not configured on the system');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
message_status: 'smpp configuration error'
|
||||
}).catch((err) => {});
|
||||
if (res) res.status(404).json({message: 'no configured SMS gateways'});
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'TaskMessage:exec - Error sending SMS');
|
||||
res.status(422).json({message: 'no configured SMS gateways'});
|
||||
this.logger.error(err, 'TaskMessage:exec - unexpected error sending SMS');
|
||||
this.performAction({
|
||||
...actionParams,
|
||||
message_status: 'system error',
|
||||
message_failure_reason: err.message
|
||||
});
|
||||
if (res) res.status(422).json({message: 'no configured SMS gateways'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
156
lib/tasks/rasa.js
Normal file
156
lib/tasks/rasa.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const bent = require('bent');
|
||||
|
||||
class Rasa extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.prompt = this.data.prompt;
|
||||
this.eventHook = this.data?.eventHook;
|
||||
this.actionHook = this.data?.actionHook;
|
||||
this.post = bent('POST', 'json', 200);
|
||||
}
|
||||
|
||||
get name() { return TaskName.Rasa; }
|
||||
|
||||
get hasReportedFinalAction() {
|
||||
return this.reportedFinalAction || this.isReplacingApplication;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
this.ep = ep;
|
||||
try {
|
||||
/* set event handlers */
|
||||
this.on('transcription', this._onTranscription.bind(this, cs, ep));
|
||||
this.on('timeout', this._onTimeout.bind(this, cs, ep));
|
||||
|
||||
/* start the first gather */
|
||||
this.gatherTask = this._makeGatherTask(this.prompt);
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
.catch((err) => this.logger.info({err}, 'Rasa gather task returned error'));
|
||||
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Rasa error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.debug('Rasa:kill');
|
||||
|
||||
if (!this.hasReportedFinalAction) {
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({rasaResult: 'caller hungup'})
|
||||
.catch((err) => this.logger.info({err}, 'rasa - error w/ action webook'));
|
||||
}
|
||||
|
||||
if (this.ep.connected) {
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
this.removeAllListeners();
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_makeGatherTask(prompt) {
|
||||
let opts = {
|
||||
input: ['speech'],
|
||||
timeout: this.data.timeout || 10,
|
||||
recognizer: this.data.recognizer || {
|
||||
vendor: 'default',
|
||||
language: 'default'
|
||||
}
|
||||
};
|
||||
if (prompt) {
|
||||
const sayOpts = this.data.tts ?
|
||||
{text: prompt, synthesizer: this.data.tts} :
|
||||
{text: prompt};
|
||||
|
||||
opts = {
|
||||
...opts,
|
||||
say: sayOpts
|
||||
};
|
||||
}
|
||||
//this.logger.debug({opts}, 'constructing a nested gather object');
|
||||
const gather = makeTask(this.logger, {gather: opts}, this);
|
||||
return gather;
|
||||
}
|
||||
|
||||
async _onTranscription(cs, ep, evt) {
|
||||
//this.logger.debug({evt}, `Rasa: got transcription for callSid ${cs.callSid}`);
|
||||
const utterance = evt.alternatives[0].transcript;
|
||||
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'userMessage', message: utterance})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Rasa_onTranscription: event handler for user message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({rasaResult: 'redirect'}, false);
|
||||
if (this.gatherTask) this.gatherTask.kill(cs);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Rasa_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
sender: cs.callSid,
|
||||
message: utterance
|
||||
};
|
||||
this.logger.debug({payload}, 'Rasa:_onTranscription - sending payload to Rasa');
|
||||
const response = await this.post(this.data.url, payload);
|
||||
this.logger.debug({response}, 'Rasa:_onTranscription - got response from Rasa');
|
||||
const botUtterance = Array.isArray(response) ?
|
||||
response.reduce((prev, current) => {
|
||||
return current.text ? `${prev} ${current.text}` : '';
|
||||
}, '') :
|
||||
null;
|
||||
if (botUtterance) {
|
||||
this.logger.debug({botUtterance}, 'Rasa:_onTranscription: got user utterance');
|
||||
this.gatherTask = this._makeGatherTask(botUtterance);
|
||||
this.gatherTask.exec(cs, ep, this)
|
||||
.catch((err) => this.logger.info({err}, 'Rasa gather task returned error'));
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'botMessage', message: response})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Rasa_onTranscription: event handler for bot message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({rasaResult: 'redirect'}, false);
|
||||
if (this.gatherTask) this.gatherTask.kill(cs);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Rasa_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Rasa_onTranscription: Error sending user utterance to Rasa - ending task');
|
||||
this.performAction({rasaResult: 'webhookError'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
_onTimeout(cs, ep, evt) {
|
||||
this.logger.debug({evt}, 'Rasa: got timeout');
|
||||
if (!this.hasReportedFinalAction) this.performAction({rasaResult: 'timeout'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = Rasa;
|
||||
@@ -19,14 +19,21 @@ class TaskSay extends Task {
|
||||
|
||||
const {srf} = cs;
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType} = srf.locals;
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
const vendor = this.synthesizer.vendor || cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language || cs.speechSynthesisLanguage;
|
||||
const voice = this.synthesizer.voice || cs.speechSynthesisVoice;
|
||||
const vendor = ('default' === this.synthesizer.vendor || !this.synthesizer.vendor) ?
|
||||
cs.speechSynthesisVendor :
|
||||
this.synthesizer.vendor;
|
||||
const language = ('default' === this.synthesizer.language || !this.synthesizer.language) ?
|
||||
cs.speechSynthesisLanguage :
|
||||
this.synthesizer.language;
|
||||
const voice = ('default' === this.synthesizer.voice || !this.synthesizer.voice) ?
|
||||
cs.speechSynthesisVoice :
|
||||
this.synthesizer.voice;
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
this.logger.info({vendor, credentials}, 'Task:say - using vendor');
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
@@ -40,7 +47,7 @@ class TaskSay extends Task {
|
||||
// synthesize all of the text elements
|
||||
let lastUpdated = false;
|
||||
const filepath = (await Promise.all(this.text.map(async(text) => {
|
||||
const {filePath, servedFromCache} = await synthAudio({
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
text,
|
||||
vendor,
|
||||
language,
|
||||
@@ -68,10 +75,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 +95,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 {
|
||||
this.ep.api('uuid_break', this.ep.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"play": {
|
||||
"properties": {
|
||||
"url": "string",
|
||||
"loop": "number",
|
||||
"loop": "number|string",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
@@ -56,7 +56,7 @@
|
||||
"say": {
|
||||
"properties": {
|
||||
"text": "string|array",
|
||||
"loop": "number",
|
||||
"loop": "number|string",
|
||||
"synthesizer": "#synthesizer",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
@@ -78,7 +78,6 @@
|
||||
"say": "#say"
|
||||
},
|
||||
"required": [
|
||||
"actionHook"
|
||||
]
|
||||
},
|
||||
"conference": {
|
||||
@@ -88,6 +87,7 @@
|
||||
"startConferenceOnEnter": "boolean",
|
||||
"endConferenceOnExit": "boolean",
|
||||
"maxParticipants": "number",
|
||||
"joinMuted": "boolean",
|
||||
"actionHook": "object|string",
|
||||
"waitHook": "object|string",
|
||||
"statusEvents": "array",
|
||||
@@ -124,6 +124,10 @@
|
||||
"credentials": "object|string",
|
||||
"project": "string",
|
||||
"environment": "string",
|
||||
"region": {
|
||||
"type": "string",
|
||||
"enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"]
|
||||
},
|
||||
"lang": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string",
|
||||
@@ -224,6 +228,19 @@
|
||||
"length"
|
||||
]
|
||||
},
|
||||
"rasa": {
|
||||
"properties": {
|
||||
"url": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"tts": "#synthesizer",
|
||||
"prompt": "string",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"properties": {
|
||||
"actionHook": "object|string"
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -99,6 +117,62 @@ class Task extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
async performHook(cs, hook, results) {
|
||||
const json = await cs.requestor.request(hook, results);
|
||||
if (json && Array.isArray(json)) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.redirect(cs, tasks);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
redirect(cs, tasks) {
|
||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.isReplacingApplication = true;
|
||||
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;
|
||||
|
||||
@@ -91,7 +91,7 @@ class TaskTranscribe extends Task {
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, ep));
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"Message": "message",
|
||||
"Pause": "pause",
|
||||
"Play": "play",
|
||||
"Rasa": "rasa",
|
||||
"Redirect": "redirect",
|
||||
"RestDial": "rest:dial",
|
||||
"SipDecline": "sip:decline",
|
||||
|
||||
@@ -135,6 +135,7 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveSet,
|
||||
addToSet,
|
||||
removeFromSet,
|
||||
monitorSet,
|
||||
pushBack,
|
||||
popFront,
|
||||
removeFromList,
|
||||
@@ -178,6 +179,7 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveSet,
|
||||
addToSet,
|
||||
removeFromSet,
|
||||
monitorSet,
|
||||
pushBack,
|
||||
popFront,
|
||||
removeFromList,
|
||||
|
||||
@@ -17,7 +17,7 @@ const moment = require('moment');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo}) {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||
super();
|
||||
assert(target.type);
|
||||
|
||||
@@ -31,6 +31,8 @@ class SingleDialer extends Emitter {
|
||||
this.bindings = logger.bindings();
|
||||
|
||||
this.parentCallInfo = callInfo;
|
||||
this.accountInfo = accountInfo;
|
||||
|
||||
this.callGone = false;
|
||||
|
||||
this.callSid = uuidv4();
|
||||
@@ -62,6 +64,7 @@ class SingleDialer extends Emitter {
|
||||
opts = opts || {};
|
||||
opts.headers = opts.headers || {};
|
||||
opts.headers = {...opts.headers, 'X-Call-Sid': this.callSid};
|
||||
this.ms = ms;
|
||||
let uri, to;
|
||||
try {
|
||||
switch (this.target.type) {
|
||||
@@ -85,6 +88,12 @@ class SingleDialer extends Emitter {
|
||||
uri = `sip:${this.target.name}`;
|
||||
to = this.target.name;
|
||||
|
||||
if (this.target.overrideTo) {
|
||||
Object.assign(opts.headers, {
|
||||
'X-Override-To': this.target.overrideTo
|
||||
});
|
||||
}
|
||||
|
||||
// need to send to the SBC registered on
|
||||
const reg = await registrar.query(aor);
|
||||
if (reg) {
|
||||
@@ -181,12 +190,21 @@ class SingleDialer extends Emitter {
|
||||
this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`);
|
||||
const connectTime = this.dlg.connectTime = moment();
|
||||
|
||||
/* race condition: we were killed just as call was answered */
|
||||
if (this.killed) {
|
||||
this.logger.info(`SingleDialer:exec race condition - we were killed as call connected: ${this.callSid}`);
|
||||
const duration = moment().diff(connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
if (this.ep) this.ep.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dlg
|
||||
.on('destroy', () => {
|
||||
const duration = moment().diff(connectTime, 'seconds');
|
||||
this.logger.debug('SingleDialer:exec called party hung up');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.ep.destroy();
|
||||
this.ep && this.ep.destroy();
|
||||
})
|
||||
.on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas'))
|
||||
.on('modify', async(req, res) => {
|
||||
@@ -246,7 +264,7 @@ class SingleDialer extends Emitter {
|
||||
async _executeApp(confirmHook) {
|
||||
try {
|
||||
// retrieve set of tasks
|
||||
const tasks = await this.requestor.request(confirmHook, this.callInfo);
|
||||
const tasks = await this.requestor.request(confirmHook, this.callInfo.toJSON());
|
||||
|
||||
// verify it contains only allowed verbs
|
||||
const allowedTasks = tasks.filter((task) => {
|
||||
@@ -284,20 +302,48 @@ class SingleDialer extends Emitter {
|
||||
this.logger = logger;
|
||||
this.adulting = true;
|
||||
this.emit('adulting');
|
||||
await this.ep.unbridge()
|
||||
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
|
||||
this.ep.play('silence_stream://1000');
|
||||
if (this.ep) {
|
||||
await this.ep.unbridge()
|
||||
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
|
||||
this.ep.play('silence_stream://1000');
|
||||
}
|
||||
else {
|
||||
await this.reAnchorMedia();
|
||||
}
|
||||
const cs = new AdultingCallSession({
|
||||
logger: this.logger,
|
||||
singleDialer: this,
|
||||
application,
|
||||
callInfo: this.callInfo,
|
||||
accountInfo: this.accountInfo,
|
||||
tasks
|
||||
});
|
||||
cs.exec();
|
||||
return cs;
|
||||
}
|
||||
|
||||
async releaseMediaToSBC(remoteSdp) {
|
||||
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
|
||||
await this.dlg.modify(remoteSdp, {
|
||||
headers: {
|
||||
'X-Reason': 'release-media'
|
||||
}
|
||||
});
|
||||
this.ep.destroy()
|
||||
.then(() => this.ep = null)
|
||||
.catch((err) => this.logger.error({err}, 'SingleDialer:releaseMediaToSBC: Error destroying endpoint'));
|
||||
}
|
||||
|
||||
async reAnchorMedia() {
|
||||
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
await this.dlg.modify(this.ep.local.sdp, {
|
||||
headers: {
|
||||
'X-Reason': 'anchor-media'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
@@ -320,9 +366,9 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo}) {
|
||||
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||
const myOpts = deepcopy(opts);
|
||||
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo});
|
||||
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo, accountInfo});
|
||||
sd.exec(srf, ms, myOpts);
|
||||
return sd;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,15 @@ const debug = require('debug')('jambonz:feature-server');
|
||||
module.exports = (logger) => {
|
||||
logger = logger || noopLogger;
|
||||
let idxSbc = 0;
|
||||
let sbcs = [];
|
||||
|
||||
assert.ok(process.env.JAMBONES_SBCS, 'missing JAMBONES_SBCS env var');
|
||||
const sbcs = process.env.JAMBONES_SBCS
|
||||
.split(',')
|
||||
.map((sbc) => sbc.trim());
|
||||
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
if (process.env.JAMBONES_SBCS) {
|
||||
sbcs = process.env.JAMBONES_SBCS
|
||||
.split(',')
|
||||
.map((sbc) => sbc.trim());
|
||||
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
||||
logger.info({sbcs}, 'SBC inventory');
|
||||
}
|
||||
|
||||
// listen for SNS lifecycle changes
|
||||
let lifecycleEmitter = new Emitter();
|
||||
@@ -96,8 +98,19 @@ module.exports = (logger) => {
|
||||
}, 20000);
|
||||
|
||||
// initial ping once we are up
|
||||
setTimeout(() => {
|
||||
setTimeout(async() => {
|
||||
const {srf} = require('../..');
|
||||
|
||||
// if SBCs are auto-scaling, monitor them as they come and go
|
||||
if (!process.env.JAMBONES_SBCS) {
|
||||
const {monitorSet} = srf.locals.dbHelpers;
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-sip`;
|
||||
await monitorSet(setName, 10, (members) => {
|
||||
sbcs = members;
|
||||
logger.info(`sbc-pinger: SBC roster has changed, list of active SBCs is now ${sbcs}`);
|
||||
});
|
||||
}
|
||||
|
||||
pingProxies(srf);
|
||||
}, 1000);
|
||||
|
||||
|
||||
4825
package-lock.json
generated
4825
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.3.1",
|
||||
"version": "0.6.7-rc3",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.6.13",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.8",
|
||||
"@jambonz/stats-collector": "^0.1.5",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"aws-sdk": "^2.846.0",
|
||||
@@ -37,7 +37,7 @@
|
||||
"debug": "^4.3.1",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^2.0.7",
|
||||
"drachtio-srf": "^4.4.50",
|
||||
"drachtio-srf": "^4.4.55",
|
||||
"express": "^4.17.1",
|
||||
"ip": "^1.1.5",
|
||||
"moment": "^2.29.1",
|
||||
|
||||
Reference in New Issue
Block a user