const Task = require('./task'); const makeTask = require('./make_task'); const {CallStatus, CallDirection, TaskName, TaskPreconditions} = require('../utils/constants'); const assert = require('assert'); const placeCall = require('../utils/place-outdial'); const config = require('config'); const moment = require('moment'); const debug = require('debug')('jambonz:feature-server'); class TaskDial extends Task { constructor(logger, opts) { super(logger, opts); this.preconditions = TaskPreconditions.None; this.earlyMedia = this.data.answerOnBridge === true; this.callerId = this.data.callerId; this.dialMusic = this.data.dialMusic; this.headers = this.data.headers || {}; this.method = this.data.method || 'POST'; this.statusCallback = this.data.statusCallback; this.statusCallbackMethod = this.data.statusCallbackMethod || 'POST'; this.target = this.data.target; this.timeout = this.data.timeout || 60; this.timeLimit = this.data.timeLimit; this.url = this.data.url; if (this.data.listen) { this.listenTask = makeTask(logger, {'listen': this.data.listen}); } if (this.data.transcribe) { this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe}); } this.results = {}; this.bridged = false; this.dials = new Map(); } get name() { return TaskName.Dial; } async exec(cs) { super.exec(cs); try { if (cs.direction === CallDirection.Inbound) { await this._initializeInbound(cs); } await this._attemptCalls(cs); await this.awaitTaskDone(); this.performAction(this.method, this.results); } catch (err) { this.logger.error(`TaskDial:exec terminating with error ${err.message}`); this.kill(); } } async kill() { super.kill(); if (this.connectTime) { const duration = moment().diff(this.connectTime, 'seconds'); this.results.dialCallDuration = duration; this.logger.debug(`Dial:kill call ended after ${duration} seconds`); } this._killOutdials(); if (this.dlg) { assert(this.ep); if (this.dlg.connected) this.dlg.destroy(); debug(`Dial:kill deleting endpoint ${this.ep.uuid}`); this.ep.destroy(); } if (this.listenTask) await this.listenTask.kill(); if (this.transcribeTask) await this.transcribeTask.kill(); this.notifyTaskDone(); } async _initializeInbound(cs) { const {ep} = await cs.connectInboundCallToIvr(this.earlyMedia); this.epOther = ep; debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`); if (this.dialMusic) { // play dial music to caller while we outdial ep.play(this.dialMusic).catch((err) => { this.logger.error(err, `TaskDial:_initializeInbound - error playing ${this.dialMusic}`); }); } } async _attemptCalls(cs) { const {req, srf} = cs; const sbcAddress = cs.direction === CallDirection.Inbound ? `${req.source_address}:${req.source_port}` : config.get('sbcAddress'); const opts = { headers: this.headers, proxy: `sip:${sbcAddress}`, callingNumber: this.callerId || req.callingNumber }; // construct bare-bones callInfo for the new outbound call attempt const callInfo = Object.assign({}, cs.callInfo); callInfo.parentCallSid = cs.callSid; callInfo.direction = CallDirection.Outbound; ['callSid', 'callID', 'from', 'to', 'callerId', 'sipStatus', 'callStatus'].forEach((k) => delete callInfo[k]); const ms = await cs.getMS(); this.target.forEach((t) => { try { t.url = t.url || this.url; const sd = placeCall({ logger: this.logger, application: cs.application, srf, ms, sbcAddress, target: t, opts, callInfo }); this.dials.set(sd.callSid, sd); sd .on('callStatusChange', (obj) => { switch (obj.callStatus) { case CallStatus.Trying: break; case CallStatus.EarlyMedia: if (this.target.length === 1 && !this.target[0].url && !this.dialMusic) { this._bridgeEarlyMedia(sd); } break; case CallStatus.InProgress: this.logger.debug('Dial:_attemptCall -- call was answered'); break; case CallStatus.Failed: case CallStatus.Busy: case CallStatus.NoAnswer: this.dials.delete(sd.callSid); if (this.dials.size === 0 && !this.connectTime) { this.logger.debug('Dial:_attemptCalls - all calls failed after call failure, ending task'); this.kill(); } break; } if (this.results.dialCallStatus !== CallStatus.Completed) { Object.assign(this.results, { dialCallStatus: obj.callStatus, dialCallSid: sd.callSid, }); } }) .on('accept', () => { this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`); this._connectSingleDial(cs, sd); }) .on('decline', () => { this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`); this.dials.delete(sd.callSid); if (this.dials.size === 0 && !this.connectTime) { this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending task'); this.kill(); } }); } catch (err) { this.logger.error(err, 'Dial:_attemptCalls'); } }); } _connectSingleDial(cs, sd) { if (!this.bridged) { this.logger.debug('Dial:_connectSingleDial bridging endpoints'); this.epOther.api('uuid_break', this.epOther.uuid); this.epOther.bridge(sd.ep); this.bridged = true; } // ding! ding! ding! we have a winner this._selectSingleDial(cs, sd); this._killOutdials(); // NB: order is important } _selectSingleDial(cs, sd) { this.connectTime = moment(); this.dials.delete(sd.callSid); debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`); this.ep = sd.ep; this.dlg = sd.dlg; this.callSid = sd.callSid; if (this.earlyMedia) { debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected'); cs.propagateAnswer(); } this.dlg.on('destroy', () => { this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation'); this.ep.unbridge(); this.kill(); }); Object.assign(this.results, { dialCallStatus: CallStatus.Completed, dialCallSid: sd.callSid, }); if (this.transcribeTask) this.transcribeTask.exec(cs, this.ep, this); if (this.listenTask) this.listenTask.exec(cs, this.ep, this); } _killOutdials() { for (const [callSid, sd] of Array.from(this.dials)) { this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`); sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`)); } this.dials.clear(); } _bridgeEarlyMedia(sd) { if (this.epOther && !this.bridged) { this.epOther.api('uuid_break', this.epOther.uuid); this.epOther.bridge(sd.ep); this.bridged = true; } } } module.exports = TaskDial;