diff --git a/lib/middleware.js b/lib/middleware.js index 49b7f48e..5f911c5f 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -7,7 +7,7 @@ const parseUri = require('drachtio-srf').parseUri; const normalizeJambones = require('./utils/normalize-jambones'); module.exports = function(srf, logger) { - const {lookupAppByPhoneNumber, lookupAppBySid, lookupAppByRealm} = srf.locals.dbHelpers; + const {lookupAppByPhoneNumber, lookupAppBySid, lookupAppByRealm, lookupAppByTeamsTenant} = srf.locals.dbHelpers; function initLocals(req, res, next) { const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4(); @@ -21,6 +21,7 @@ module.exports = function(srf, logger) { req.locals.application_sid = application_sid; } if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User'); + if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN'); next(); } @@ -64,6 +65,10 @@ module.exports = function(srf, logger) { } } + else if (req.locals.msTeamsTenant) { + app = await lookupAppByTeamsTenant(req.locals.msTeamsTenant); + if (app) logger.debug({app}, `retrieved app for ms teams tenant ${req.locals.msTeamsTenant}`); + } else { const uri = parseUri(req.uri); const arr = /context-(.*)/.exec(uri.user); diff --git a/lib/tasks/dial.js b/lib/tasks/dial.js index e59f2051..93d99a09 100644 --- a/lib/tasks/dial.js +++ b/lib/tasks/dial.js @@ -39,6 +39,7 @@ function compareTasks(t1, t2) { case 'phone': return t1.number === t1.number; case 'user': + case 'teams': return t2.name === t1.name; case 'sip': return t2.sipUri === t1.sipUri; @@ -243,7 +244,9 @@ class TaskDial extends Task { async _attemptCalls(cs) { const {req, srf} = cs; const {getSBC} = srf.locals; + const {lookupTeamsByAccount} = srf.locals.dbHelpers; const sbcAddress = getSBC(); + const teamsInfo = {}; if (!sbcAddress) throw new Error('no SBC found for outbound call'); const opts = { @@ -252,6 +255,12 @@ class TaskDial extends Task { callingNumber: this.callerId || req.callingNumber }; + if (this.target.find((t) => t.type === 'teams')) { + const obj = await lookupTeamsByAccount(cs.accountSid); + if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info'); + Object.assign(teamsInfo, {tenant_fqdn: obj.tenant_fqdn, ms_teams_fqdn: obj.ms_teams_fqdn}); + } + const ms = await cs.getMS(); const timerRing = setTimeout(() => { this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`); @@ -262,6 +271,7 @@ class TaskDial extends Task { try { t.url = t.url || this.confirmUrl; t.method = t.method || this.confirmMethod || 'POST'; + if (t.type === 'teams') t.teamsInfo = teamsInfo; const sd = placeCall({ logger: this.logger, application: cs.application, @@ -393,6 +403,7 @@ class TaskDial extends Task { _bridgeEarlyMedia(sd) { if (this.epOther && !this.bridged) { this.epOther.api('uuid_break', this.epOther.uuid); + this.logger.debug('Dial:_bridgeEarlyMedia: bridging early media'); this.epOther.bridge(sd.ep); this.bridged = true; } diff --git a/lib/tasks/specs.json b/lib/tasks/specs.json index 65688530..e485d34e 100644 --- a/lib/tasks/specs.json +++ b/lib/tasks/specs.json @@ -202,7 +202,7 @@ "properties": { "type": { "type": "string", - "enum": ["phone", "sip", "user"] + "enum": ["phone", "sip", "user", "teams"] }, "url": "string", "method": { @@ -212,7 +212,8 @@ "name": "string", "number": "string", "sipUri": "string", - "auth": "#auth" + "auth": "#auth", + "vmail": "boolean" }, "required": [ "type" diff --git a/lib/utils/install-srf-locals.js b/lib/utils/install-srf-locals.js index d0077b22..808666ff 100644 --- a/lib/utils/install-srf-locals.js +++ b/lib/utils/install-srf-locals.js @@ -101,8 +101,10 @@ function installSrfLocals(srf, logger) { const { lookupAppByPhoneNumber, lookupAppBySid, - lookupAppByRealm - } = require('jambonz-db-helpers')({ + lookupAppByRealm, + lookupAppByTeamsTenant, + lookupTeamsByAccount + } = require('@jambonz/db-helpers')({ host: process.env.JAMBONES_MYSQL_HOST, user: process.env.JAMBONES_MYSQL_USER, password: process.env.JAMBONES_MYSQL_PASSWORD, @@ -138,6 +140,8 @@ function installSrfLocals(srf, logger) { lookupAppByPhoneNumber, lookupAppBySid, lookupAppByRealm, + lookupAppByTeamsTenant, + lookupTeamsByAccount, updateCallStatus, retrieveCall, listCalls, diff --git a/lib/utils/place-outdial.js b/lib/utils/place-outdial.js index 77a5edc3..610cf8e5 100644 --- a/lib/utils/place-outdial.js +++ b/lib/utils/place-outdial.js @@ -58,13 +58,24 @@ class SingleDialer extends Emitter { } async exec(srf, ms, opts) { + opts = opts || {}; let uri, to; try { switch (this.target.type) { case 'phone': + case 'teams': assert(this.target.number); uri = `sip:${this.target.number}@${this.sbcAddress}`; to = this.target.number; + if ('teams' === this.target.type) { + assert(this.target.teamsInfo); + opts.headers = opts.headers || {}; + Object.assign(opts.headers, { + 'X-MS-Teams-FQDN': this.target.teamsInfo.ms_teams_fqdn, + 'X-MS-Teams-Tenant-FQDN': this.target.teamsInfo.tenant_fqdn + }); + if (this.target.vmail === true) uri = `${uri};opaque=app:voicemail`; + } break; case 'user': assert(this.target.name); @@ -156,12 +167,23 @@ class SingleDialer extends Emitter { this.logger.debug(`SingleDialer:exec call connected: ${this.callSid}`); const connectTime = this.dlg.connectTime = moment(); - 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.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(); + }) + .on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas')) + .on('modify', async(req, res) => { + try { + const newSdp = await this.ep.modify(req.body); + res.send(200, {body: newSdp}); + this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE'); + } catch (err) { + this.logger.error(err, 'Error handling reinvite'); + } + }); if (this.confirmHook) this._executeApp(this.confirmHook); else this.emit('accept'); diff --git a/package-lock.json b/package-lock.json index 7aaa7815..bf07ba22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -294,6 +294,23 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@jambonz/db-helpers": { + "version": "0.3.5", + "resolved": "https://npm.pkg.github.com/download/@jambonz/db-helpers/0.3.5/c91102adbb4850844cc84db212b80884f5c9944de2200c247c606ad5e3037c90", + "integrity": "sha512-7wp91KmtChAO9RyFm/Ud8vWPlur+W6+8HocXo1ozeQmIqd9U3+Tlww0Jsgn+UjxeKPq09qHejTXaHGBRiwjn0g==", + "requires": { + "debug": "^4.1.1", + "mysql2": "^2.0.2", + "uuid": "^7.0.3" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -355,9 +372,9 @@ "dev": true }, "@types/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", + "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==", "requires": { "@types/node": "*" } @@ -368,9 +385,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "13.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz", - "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==" + "version": "13.13.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.6.tgz", + "integrity": "sha512-zqRj8ugfROCjXCNbmPBe2mmQ0fJWP9lQaN519hwunOgpHgVykme4G6FW95++dyNFDvJUk4rtExkVkL0eciu5NA==" }, "abort-controller": { "version": "3.0.0", @@ -519,9 +536,9 @@ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, "aws-sdk": { - "version": "2.669.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.669.0.tgz", - "integrity": "sha512-kuVcSRpDzvkgmeSmMX6Q32eTOb8UeihhUdavMrvUOP6fzSU19cNWS9HAIkYOi/jrEDK85cCZxXjxqE3JGZIGcw==", + "version": "2.679.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.679.0.tgz", + "integrity": "sha512-bsbu3Mbwqz2DHpVSSgaSoFYsNrW+54vXN74iG++cCE7AAg9vOgCKzGul8yhR4jxxlOmGlfP9kCNrkCpjQ/Uqkg==", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -2365,15 +2382,6 @@ "istanbul-lib-report": "^3.0.0" } }, - "jambonz-db-helpers": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/jambonz-db-helpers/-/jambonz-db-helpers-0.3.2.tgz", - "integrity": "sha512-j7AEgts+Bj1CFPiM0estFmWmdDTZKWbkeIPY1QT3BR0cLClzjqo9fmdCzLoDtk/NWMy7IPNEQpVHzEejxFHq9g==", - "requires": { - "debug": "^4.1.1", - "mysql2": "^2.0.2" - } - }, "jambonz-mw-registrar": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/jambonz-mw-registrar/-/jambonz-mw-registrar-0.1.3.tgz", @@ -2384,9 +2392,9 @@ } }, "jambonz-realtimedb-helpers": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/jambonz-realtimedb-helpers/-/jambonz-realtimedb-helpers-0.2.12.tgz", - "integrity": "sha512-KcuXPSwxc9cHXz9uzuI9HxnHcKRHbwwq7BSFNZT61lF7KZdZDBozenDpP0Tmk/P65SN1gLbtWP4KM2W4FTtv/Q==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/jambonz-realtimedb-helpers/-/jambonz-realtimedb-helpers-0.2.13.tgz", + "integrity": "sha512-7BAqnauT1DPjgW4OnLIUgJMxmJzABlxSWWXfmrki4QYc0GG/a3iV9JyDd4jtiumWtvygUaUmbht1AdnqLWnrJw==", "requires": { "@google-cloud/text-to-speech": "^2.2.0", "aws-sdk": "^2.631.0", @@ -3314,9 +3322,9 @@ } }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safer-buffer": { "version": "2.1.2", diff --git a/package.json b/package.json index c57c6a38..9cd386d1 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "drachtio-srf": "^4.4.28", "express": "^4.17.1", "ip": "^1.1.5", - "jambonz-db-helpers": "^0.3.2", + "@jambonz/db-helpers": "^0.3.7", "jambonz-mw-registrar": "^0.1.3", "jambonz-realtimedb-helpers": "^0.2.13", "jambonz-stats-collector": "^0.0.3",