From d5699034b9ca19f885b60160ac6830d7bf8207e9 Mon Sep 17 00:00:00 2001 From: Dele Olajide Date: Sun, 2 Aug 2020 13:40:10 +0100 Subject: [PATCH] prepare for ver 1.0.0 --- config/pom.xml | 2 +- offocus/pom.xml | 2 +- ofgasi/pom.xml | 2 +- ofmeet/pom.xml | 2 +- pade/classes/public/changelog.html | 4 +- pade/classes/public/credentials.js | 10 +- pade/classes/public/index.html | 6 - pade/classes/public/index.js | 153 ------- pade/classes/public/inverse/chrome.js | 9 - pade/classes/public/inverse/dist/converse.js | 2 +- pade/classes/public/inverse/index.html | 1 + pade/classes/public/inverse/index.js | 67 ++-- pade/classes/public/inverse/serviceworker.js | 109 +++++ pade/classes/public/inverse/webpush.js | 153 +++++++ pade/classes/public/js/background.js | 374 +++--------------- pade/classes/public/options/settings.js | 3 +- pade/classes/public/webpush.js | 153 +++++++ pade/pom.xml | 2 +- .../java/nl/martijndwars/webpush/Stanza.java | 15 + .../java/nl/martijndwars/webpush/Utils.java | 2 +- .../pushnotification/PushInterceptor.java | 197 +++++++++ .../openfire/plugins/pade/PadePlugin.java | 65 ++- pom.xml | 2 +- videobridge/pom.xml | 2 +- 24 files changed, 772 insertions(+), 565 deletions(-) create mode 100644 pade/classes/public/inverse/serviceworker.js create mode 100644 pade/classes/public/inverse/webpush.js create mode 100644 pade/classes/public/webpush.js create mode 100644 pade/src/java/nl/martijndwars/webpush/Stanza.java create mode 100644 pade/src/java/org/igniterealtime/openfire/plugins/pushnotification/PushInterceptor.java diff --git a/config/pom.xml b/config/pom.xml index 8c641fd9..d776123b 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -20,7 +20,7 @@ org.igniterealtime.openfire.ofmeet parent - 0.9.13-SNAPSHOT + 1.0.0-SNAPSHOT config diff --git a/offocus/pom.xml b/offocus/pom.xml index 131deb69..44d23976 100644 --- a/offocus/pom.xml +++ b/offocus/pom.xml @@ -21,7 +21,7 @@ org.igniterealtime.openfire.ofmeet parent - 0.9.13-SNAPSHOT + 1.0.0-SNAPSHOT offocus diff --git a/ofgasi/pom.xml b/ofgasi/pom.xml index 97747a84..99445d9c 100644 --- a/ofgasi/pom.xml +++ b/ofgasi/pom.xml @@ -23,7 +23,7 @@ org.igniterealtime.openfire.ofmeet parent - 0.9.13-SNAPSHOT + 1.0.0-SNAPSHOT ofgasi diff --git a/ofmeet/pom.xml b/ofmeet/pom.xml index 0d06b13e..5df593f2 100644 --- a/ofmeet/pom.xml +++ b/ofmeet/pom.xml @@ -19,7 +19,7 @@ org.igniterealtime.openfire.ofmeet parent - 0.9.13-SNAPSHOT + 1.0.0-SNAPSHOT ofmeet diff --git a/pade/classes/public/changelog.html b/pade/classes/public/changelog.html index 3794749c..e3670b33 100644 --- a/pade/classes/public/changelog.html +++ b/pade/classes/public/changelog.html @@ -48,8 +48,10 @@

Changelog

-

1.6.8 -- July ??, 2020

+

1.6.8 -- July 31, 2020

1.6.7 -- June 28, 2020

diff --git a/pade/classes/public/credentials.js b/pade/classes/public/credentials.js index e0ce240f..f3020931 100644 --- a/pade/classes/public/credentials.js +++ b/pade/classes/public/credentials.js @@ -5,7 +5,15 @@ function getCredentials(username, password, callback) navigator.credentials.get({password: true, federated: {providers: [ 'https://accounts.google.com' ]}}).then(function(credential) { console.log("credential management api get", credential); - if (callback) callback(credential || {err: !credential, id: username, password: password, anonymous: !password}); + const creds = {err: !credential, id: username, password: password, anonymous: !password}; + + if (credential) + { + creds.id = credential.id.split("@")[0]; + creds.password = credential.password; + } + + if (callback) callback(creds); }).catch(function(err){ console.error ("credential management api get error", err); diff --git a/pade/classes/public/index.html b/pade/classes/public/index.html index 2c51f00e..c63c153e 100644 --- a/pade/classes/public/index.html +++ b/pade/classes/public/index.html @@ -39,12 +39,6 @@ - - - - - - diff --git a/pade/classes/public/index.js b/pade/classes/public/index.js index c0b88b3d..4560b2ee 100644 --- a/pade/classes/public/index.js +++ b/pade/classes/public/index.js @@ -9,156 +9,3 @@ window.addEventListener("load", function() document.title = chrome.i18n.getMessage('manifest_shortExtensionName') + " | " + chrome.runtime.getManifest().version; }); -var webpush = (function(push) -{ - var hostname, username, password, publicKey; - - function vapidGetPublicKey(host, user, pass) - { - var getUrl = "https://" + host + "/rest/api/restapi/v1/meet/webpush/" + user; - var options = {method: "GET", headers: {"Authorization": "Basic " + btoa(user + ":" + pass), "Accept":"application/json", "Content-Type":"application/json"}}; - - console.debug("vapidGetPublicKey", getUrl, options); - - fetch(getUrl, options).then(function(response) {return response.json()}).then(function(vapid) - { - if (vapid.publicKey) - { - console.debug("vapidGetPublicKey found", vapid); - - publicKey = vapid.publicKey; - hostname = host; - username = user; - password = pass; - - navigator.serviceWorker.register('./serviceworker.js', {scope: './'}).then(initialiseState, initialiseError); - } else { - console.error("no web push, vapid public key not available"); - } - - }).catch(function (err) { - console.error('vapidGetPublicKey error!', err); - }); - } - - function initialiseError(error) - { - console.error("initialiseError", error); - } - - function initialiseState(registration) - { - if (!('showNotification' in ServiceWorkerRegistration.prototype)) { - console.warn('Notifications aren\'t supported.'); - return; - } - - if (Notification.permission === 'denied') { - console.warn('The user has blocked notifications.'); - return; - } - - if (!('PushManager' in window)) { - console.warn('Push messaging isn\'t supported.'); - return; - } - - console.debug("initialiseState", registration); - - navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) - { - console.debug("initialiseState ready", serviceWorkerRegistration); - - serviceWorkerRegistration.pushManager.getSubscription().then(function (subscription) - { - console.debug("serviceWorkerRegistration getSubscription", subscription); - - if (!subscription && publicKey) { - subscribe(); - return; - } - - // Keep your server in sync with the latest subscriptionId - sendSubscriptionToServer(subscription); - }) - .catch(function(err) { - console.warn('Error during getSubscription()', err); - }); - }); - } - - function subscribe() - { - console.debug("subscribe", publicKey); - - navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) - { - serviceWorkerRegistration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: base64UrlToUint8Array(publicKey) - }) - .then(function (subscription) { - return sendSubscriptionToServer(subscription); - }) - .catch(function (e) { - if (Notification.permission === 'denied') { - console.warn('Permission for Notifications was denied'); - } else { - console.error('Unable to subscribe to push.', e); - } - }); - }); - } - - function base64UrlToUint8Array(base64UrlData) - { - const padding = '='.repeat((4 - base64UrlData.length % 4) % 4); - const base64 = (base64UrlData + padding) - .replace(/\-/g, '+') - .replace(/_/g, '/'); - - const rawData = atob(base64); - const buffer = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - buffer[i] = rawData.charCodeAt(i); - } - - return buffer; - } - - function sendSubscriptionToServer(subscription) - { - console.debug("sendSubscriptionToServer", subscription); - - var key = subscription.getKey ? subscription.getKey('p256dh') : ''; - var auth = subscription.getKey ? subscription.getKey('auth') : ''; - - var subscriptionString = JSON.stringify(subscription); // TODO - - console.debug("web push subscription", { - endpoint: subscription.endpoint, - key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '', - auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : '' - }, subscription); - - var resource = chrome.i18n.getMessage('manifest_shortExtensionName').toLowerCase() + "-" + BrowserDetect.browser + BrowserDetect.version + BrowserDetect.OS; - var putUrl = "https://" + hostname + "/rest/api/restapi/v1/meet/webpush/" + username + "/" + resource; - var options = {method: "PUT", body: JSON.stringify(subscription), headers: {"Authorization": "Basic " + btoa(username + ":" + password), "Accept":"application/json", "Content-Type":"application/json"}}; - - return fetch(putUrl, options).then(function(response) { - console.debug("subscribe response", response); - - }).catch(function (err) { - console.error('subscribe error!', err); - }); - } - - push.registerServiceWorker = function(host, username, password) - { - vapidGetPublicKey(host, username, password); - } - - return push; - -}(webpush || {})); \ No newline at end of file diff --git a/pade/classes/public/inverse/chrome.js b/pade/classes/public/inverse/chrome.js index fb1e00cb..2848fa2e 100644 --- a/pade/classes/public/inverse/chrome.js +++ b/pade/classes/public/inverse/chrome.js @@ -329,15 +329,6 @@ function getSetting(name, defaultValue) { var localStorage = window.localStorage //console.debug("getSetting", name, defaultValue, localStorage["store.settings." + name]); - - if (window.pade) - { - if (name == "username") return window.pade.username; - if (name == "password") return window.pade.password; - if (name == "domain") return window.pade.domain; - if (name == "server") return window.pade.server; - } - var value = defaultValue; if (localStorage["store.settings." + name]) diff --git a/pade/classes/public/inverse/dist/converse.js b/pade/classes/public/inverse/dist/converse.js index 7d47898b..329d5690 100644 --- a/pade/classes/public/inverse/dist/converse.js +++ b/pade/classes/public/inverse/dist/converse.js @@ -73105,7 +73105,7 @@ converse.plugins.add('converse-emoji-views', { return; } - if (!_converse.emojipicker) { + if (!_converse.emojipicker && _converse.emojis.json) { _converse.emojis.json.recent = {}; // BAO const id = "converse.emoji-".concat(_converse.bare_jid); _converse.emojipicker = new _converse.EmojiPicker({ diff --git a/pade/classes/public/inverse/index.html b/pade/classes/public/inverse/index.html index 5f4ee597..f95e683f 100644 --- a/pade/classes/public/inverse/index.html +++ b/pade/classes/public/inverse/index.html @@ -12,6 +12,7 @@ + diff --git a/pade/classes/public/inverse/index.js b/pade/classes/public/inverse/index.js index f420e1a7..1703aca4 100644 --- a/pade/classes/public/inverse/index.js +++ b/pade/classes/public/inverse/index.js @@ -95,27 +95,9 @@ var padeapi = (function(api) setDefaultSetting("server", location.host); setDefaultSetting("domain", location.hostname); - - if (typeof parent.getCredentials == 'function') - { - parent.getCredentials(username, password, function(credential) - { - if ((credential.id && credential.password) || credential.anonymous) - { - doConverse(server, credential.id, credential.password, credential.anonymous && anonUser); - } - else { - doBasicAuth(); - } - }); - } else { - anonUser = true; - doBasicAuth(); - } - } - else { - doBasicAuth(); } + + doBasicAuth(); }); window.addEventListener('message', function (event) @@ -823,27 +805,26 @@ var padeapi = (function(api) background.$iq = $iq; background.$msg = $msg; background.$pres = $pres; - background.pade.connection = _converse.connection; background.setupUserPayment(); background.setupStreamDeck(); - if (chrome.pade) // browser mode + const id = Strophe.getNodeFromJid(_converse.connection.jid); + const password = _converse.connection.pass; + + if (id && password && webpush && webpush.registerServiceWorker) // register webpush service worker { - const id = Strophe.getBareJidFromJid(_converse.connection.jid); - const password = _converse.connection.pass; + webpush.registerServiceWorker(getSetting("server"), username, password); + } + if (chrome.pade) // browser mode + { if (id && password) { if (parent.setCredentials) // save new credentials { parent.setCredentials({id: id, password: password}); } - - if (parent.webpush && parent.webpush.registerServiceWorker) // register webpush service worker - { - parent.webpush.registerServiceWorker(getSetting("server"), username, password); - } } if (typeof module === 'object') // electron fix for jQuery @@ -932,6 +913,11 @@ var padeapi = (function(api) console.debug("addControlFeatures", section); + if (getSetting("converseSimpleView", false)) + { + handleActiveConversations(); + } + const viewButton = __newElement('a', null, ''); section.appendChild(viewButton); @@ -943,10 +929,15 @@ var padeapi = (function(api) }, false); - if (getSetting("converseSimpleView", false)) + const ofmeetButton = __newElement('a', null, ''); + section.appendChild(ofmeetButton); + + ofmeetButton.addEventListener('click', function(evt) { - handleActiveConversations(); - } + evt.stopPropagation(); + background.openVideoWindow("", "normal"); + + }, false); const prefButton = __newElement('a', null, ''); section.appendChild(prefButton); @@ -1942,16 +1933,14 @@ var padeapi = (function(api) function listenForRoomActivityIndicators() { - console.debug("listenForRoomActivityIndicators"); - _converse.connection.addHandler(function(message) { + console.debug("listenForRoomActivityIndicators - message", message); + message.querySelectorAll('activity').forEach(function(activity) { - if (activity && activity.getAttribute("xmlns") == "xmpp:prosody.im/protocol/rai") { + if (activity) { const jid = activity.innerHTML; - _converse.api.trigger('chatRoomActivityIndicators', jid); - console.debug("listenForRoomActivityIndicators - message", jid); if (_converse.api.settings.get("rai_notification")) @@ -1971,7 +1960,7 @@ var padeapi = (function(api) return true; - }, null, 'message', 'groupchat'); + }, 'xmpp:prosody.im/protocol/rai', 'message'); } function sendMarker(to_jid, id, type) @@ -2253,7 +2242,7 @@ var padeapi = (function(api) var opt = { type: "basic", title: title, - iconUrl: chrome.runtime.getURL("image.png"), + iconUrl: chrome.runtime.getURL ? chrome.runtime.getURL("image.png") : "../image.png", message: message, buttons: buttons, contextMessage: chrome.i18n.getMessage('manifest_extensionName'), diff --git a/pade/classes/public/inverse/serviceworker.js b/pade/classes/public/inverse/serviceworker.js new file mode 100644 index 00000000..97e3031a --- /dev/null +++ b/pade/classes/public/inverse/serviceworker.js @@ -0,0 +1,109 @@ +// install trigger for sw - cache index.html + +self.addEventListener('install', function(event) { + if (location.protocol.indexOf("http") > -1) + { + var indexPage = new Request('index.html'); + event.waitUntil( + fetch(indexPage).then(function(response) { + return caches.open('offline').then(function(cache) { + return cache.put(indexPage, response); + }); + })); + } +}); + +// activate trigger + +self.addEventListener('activate', function (event) { + console.log('Activated', event); +}); + + +// fetch trigger - serve from cache or fetch from server, cache the file if not previously cached + +self.addEventListener('fetch', function(event) { + if (location.protocol.indexOf("http") > -1 && event.request.method == "GET") event.respondWith( + fetch(event.request).then(function(response) { + return caches.open('offline').then(function(cache) { + try { + cache.put(event.request, response.clone()); + } catch (e) {}; + return response; + }); + }).catch(function (error) { + caches.match(event.request).then(function(resp) { + return resp; + }); + }) + ); +}); + +// push trigger + +self.addEventListener('push', function (event) { + console.debug('push', event); + + const data = event.data.json(); + const pos = location.href.lastIndexOf('/') + 1; + + data.url = location.protocol + "//" + location.host + "/pade"; + + console.debug('Push message', data); + + const options = { + body: data.msgBody, + icon: '../icon.png', + vibrate: [100, 50, 100], + data: data, + actions: [ + {action: 'read', title: 'Read', icon: '../images/check-solid.png'}, + {action: 'ignore', title: 'Ignore', icon: '../images/times-solid.png'}, + ] + }; + event.waitUntil( + self.registration.showNotification("Pade - " + data.msgFrom, options) + ); +}); + +self.addEventListener("pushsubscriptionchange", function(e) { + console.debug('pushsubscriptionchange', e); +}); + +self.addEventListener('notificationclose', function(e) { + console.debug('Closed notification', e.notification); +}); + +self.addEventListener('notificationclick', function(event) { + console.debug('notificationclick', event); + + event.notification.close(); + + if (event.action === 'read') + { + event.waitUntil(clients.matchAll({type: "window"}).then(function(clientList) + { + if (location.protocol.indexOf("chrome-extension") > -1) + { + const channel = new BroadcastChannel('sw-notification'); + channel.postMessage(event.notification.data); + } + else { + const url = event.notification.data.url; + + for (var i = 0; i < clientList.length; i++) { + var client = clientList[i]; + console.debug("found url", client.url, client); + + if (client.url == event.notification.data.url && client.visibilityState == "visible") { + client.postMessage(event.notification.data); + return client.focus(); + } + } + if (clients.openWindow) { + return clients.openWindow(event.notification.data.url); + } + } + })); + } +}, false); \ No newline at end of file diff --git a/pade/classes/public/inverse/webpush.js b/pade/classes/public/inverse/webpush.js new file mode 100644 index 00000000..47a260c7 --- /dev/null +++ b/pade/classes/public/inverse/webpush.js @@ -0,0 +1,153 @@ +var webpush = (function(push) +{ + var hostname, username, password, publicKey; + + function vapidGetPublicKey(host, user, pass) + { + var getUrl = "https://" + host + "/rest/api/restapi/v1/meet/webpush/" + user; + var options = {method: "GET", headers: {"Authorization": "Basic " + btoa(user + ":" + pass), "Accept":"application/json", "Content-Type":"application/json"}}; + + console.debug("vapidGetPublicKey", getUrl, options); + + fetch(getUrl, options).then(function(response) {return response.json()}).then(function(vapid) + { + if (vapid.publicKey) + { + console.debug("vapidGetPublicKey found", vapid); + + publicKey = vapid.publicKey; + hostname = host; + username = user; + password = pass; + + navigator.serviceWorker.register('./serviceworker.js', {scope: './'}).then(initialiseState, initialiseError); + } else { + console.error("no web push, vapid public key not available"); + } + + }).catch(function (err) { + console.error('vapidGetPublicKey error!', err); + }); + } + + function initialiseError(error) + { + console.error("initialiseError", error); + } + + function initialiseState(registration) + { + if (!('showNotification' in ServiceWorkerRegistration.prototype)) { + console.warn('Notifications aren\'t supported.'); + return; + } + + if (Notification.permission === 'denied') { + console.warn('The user has blocked notifications.'); + return; + } + + if (!('PushManager' in window)) { + console.warn('Push messaging isn\'t supported.'); + return; + } + + console.debug("initialiseState", registration); + + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) + { + console.debug("initialiseState ready", serviceWorkerRegistration); + + serviceWorkerRegistration.pushManager.getSubscription().then(function (subscription) + { + console.debug("serviceWorkerRegistration getSubscription", subscription); + + if (!subscription && publicKey) { + subscribe(); + return; + } + + // Keep your server in sync with the latest subscriptionId + sendSubscriptionToServer(subscription); + }) + .catch(function(err) { + console.warn('Error during getSubscription()', err); + }); + }); + } + + function subscribe() + { + console.debug("subscribe", publicKey); + + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) + { + serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: base64UrlToUint8Array(publicKey) + }) + .then(function (subscription) { + return sendSubscriptionToServer(subscription); + }) + .catch(function (e) { + if (Notification.permission === 'denied') { + console.warn('Permission for Notifications was denied'); + } else { + console.error('Unable to subscribe to push.', e); + } + }); + }); + } + + function base64UrlToUint8Array(base64UrlData) + { + const padding = '='.repeat((4 - base64UrlData.length % 4) % 4); + const base64 = (base64UrlData + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = atob(base64); + const buffer = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + buffer[i] = rawData.charCodeAt(i); + } + + return buffer; + } + + function sendSubscriptionToServer(subscription) + { + console.debug("sendSubscriptionToServer", subscription); + + var key = subscription.getKey ? subscription.getKey('p256dh') : ''; + var auth = subscription.getKey ? subscription.getKey('auth') : ''; + + var subscriptionString = JSON.stringify(subscription); // TODO + + console.debug("web push subscription", { + endpoint: subscription.endpoint, + key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '', + auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : '' + }, subscription); + + var resource = chrome.i18n.getMessage('manifest_shortExtensionName').toLowerCase() + "-" + BrowserDetect.browser + BrowserDetect.version + BrowserDetect.OS; + var putUrl = "https://" + hostname + "/rest/api/restapi/v1/meet/webpush/" + username + "/" + resource; + var options = {method: "PUT", body: JSON.stringify(subscription), headers: {"Authorization": "Basic " + btoa(username + ":" + password), "Accept":"application/json", "Content-Type":"application/json"}}; + + return fetch(putUrl, options).then(function(response) { + console.debug("subscribe response", response); + + }).catch(function (err) { + console.error('subscribe error!', err); + }); + } + + push.registerServiceWorker = function(host, username, password) + { + vapidGetPublicKey(host, username, password); + } + + return push; + +}(webpush || {})); \ No newline at end of file diff --git a/pade/classes/public/js/background.js b/pade/classes/public/js/background.js index 5ee033b3..c7330c50 100644 --- a/pade/classes/public/js/background.js +++ b/pade/classes/public/js/background.js @@ -1,4 +1,4 @@ -var pade = { +window.pade = { tasks: {}, autoJoinRooms: {}, autoJoinPrivateChats: {}, @@ -9,160 +9,15 @@ var pade = { transferWise: {} } -//pade.transferWiseUrl = "https://api.sandbox.transferwise.tech/v1"; -pade.transferWiseUrl = "https://api.transferwise.com/v1"; - -var BrowserDetect = { - init: function () { - this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; - this.version = this.searchVersion(navigator.userAgent) - || this.searchVersion(navigator.appVersion) - || "an unknown version"; - this.OS = this.searchString(this.dataOS) || "an unknown OS"; - - this.width = 0; - this.height = 0; - - if ( typeof( window.innerWidth ) == 'number' ) - { - this.width = window.innerWidth; - this.height = window.innerHeight; - - } else if ( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) { - - this.width = document.documentElement.clientWidth; - this.height = document.documentElement.clientHeight; - - } else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) { - - this.width = document.body.clientWidth; - this.height = document.body.clientHeight; - } - }, - - searchString: function (data) { - for (var i=0;i { + console.log('Received', event.data); + openChatWindow("inverse/index.html"); +}); -var callbacks = {} +//pade.transferWiseUrl = "https://api.sandbox.transferwise.tech/v1"; +pade.transferWiseUrl = "https://api.transferwise.com/v1"; window.addEventListener("unload", function () { @@ -465,48 +320,6 @@ window.addEventListener("load", function() } }); - - chrome.notifications.onClosed.addListener(function(notificationId, byUser) - { - - }); - - chrome.notifications.onButtonClicked.addListener(function(notificationId, buttonIndex) - { - var callback = callbacks[notificationId]; - - if (callback) - { - callback(notificationId, buttonIndex); - - callbacks[notificationId] = null; - delete callbacks[notificationId]; - - chrome.notifications.clear(notificationId, function(wasCleared) - { - - }); - } - }); - - chrome.notifications.onClicked.addListener(function(notificationId) - { - var callback = callbacks[notificationId]; - - if (callback) - { - callback(notificationId, -1); - - callbacks[notificationId] = null; - delete callbacks[notificationId]; - - chrome.notifications.clear(notificationId, function(wasCleared) - { - - }); - } - }); - chrome.browserAction.onClicked.addListener(function() { if (getSetting("server", null)) @@ -717,10 +530,50 @@ window.addEventListener("load", function() setupJabra(); enableRemoteControl(); runMeetingPlanner(); + doSetupStrophePlugins(); } else doExtensionPage("options/index.html"); }); +function doSetupStrophePlugins() +{ + var setupUserPass = function(username, password) + { + setSetting("username", username); + setSetting("password", password); + + pade.username = username; + pade.password = password; + + pade.jid = pade.username + "@" + pade.domain; + pade.displayName = getSetting("displayname", pade.username); + } + + if (getSetting("useWinSSO", false)) + { + var server = getSetting("server", null); + + console.debug("doSetupStrophePlugins - WinSSO", server); + + if (server) + { + fetch("https://" + server + "/sso/password", {method: "GET"}).then(function(response){ return response.text()}).then(function(accessToken) + { + console.log("Strophe.SASLOFChat.WINSSO", accessToken); + + if (accessToken.indexOf(":") > -1 ) + { + var userPass = accessToken.split(":"); + setupUserPass(userPass[0], userPass[1]); + } + + }).catch(function (err) { + console.error("Strophe.SASLOFChat.WINSSO", err); + }); + } + } +} + function handleUrlClick(info) { console.debug("handleUrlClick", info); @@ -991,125 +844,6 @@ function getVCard(jid, callback, errorback) } else if (errorback) errorback(); } -function notifyText(message, title, jid, buttons, callback, notifyId) -{ - console.debug("notifyText", title, message, jid, buttons, notifyId); - - if (pade.busy) return; // no notifications when I am busy - - var opt = { - type: "basic", - title: title, - iconUrl: chrome.runtime.getURL("image.png"), - message: message, - buttons: buttons, - contextMessage: chrome.i18n.getMessage('manifest_extensionName'), - requireInteraction: !!buttons && !!callback - } - - if (!notifyId) notifyId = Math.random().toString(36).substr(2,9); - - var doNotify = function() - { - chrome.notifications.create(notifyId, opt, function(notificationId) - { - if (chrome.pade) - { - if (callback) callback(notificationId, 0); - } - else - if (callback) callbacks[notificationId] = callback; - }); - } - - if (avatars[jid]) - { - opt.iconUrl = avatars[jid]; - doNotify(); - } - else - - if (jid && jid.indexOf("@" > -1) && jid.indexOf("@conference." == -1)) - { - getVCard(jid, function(vCard) - { - console.debug("notifyText vcard", vCard); - - if (vCard.avatar) opt.iconUrl = vCard.avatar; - doNotify(); - - }, doNotify); - } - else doNotify(); -}; - -function notifyImage(message, title, imageUrl, buttons, callback) -{ - if (pade.busy) return; // no notifications when I am busy - - var opt = { - type: "image", - title: title, - iconUrl: chrome.runtime.getURL("image.png"), - - message: message, - buttons: buttons, - contextMessage: chrome.i18n.getMessage('manifest_extensionName'), - imageUrl: imageUrl - } - var id = Math.random().toString(36).substr(2,9); - - chrome.notifications.create(id, opt, function(notificationId) - { - if (callback) callbacks[notificationId] = callback; - }); -}; - -function notifyProgress(message, title, progress, buttons, callback) -{ - if (pade.busy) return; // no notifications when I am busy - - var opt = { - type: "progress", - title: title, - iconUrl: chrome.runtime.getURL("image.png"), - - message: message, - buttons: buttons, - contextMessage: chrome.i18n.getMessage('manifest_extensionName'), - progress: progress - } - var id = Math.random().toString(36).substr(2,9); - - chrome.notifications.create(id, opt, function(notificationId) - { - if (callback) callbacks[notificationId] = callback; - }); -}; - - -function notifyList(message, title, items, buttons, callback, notifyId) -{ - if (pade.busy) return; // no notifications when I am busy - - var opt = { - type: "list", - title: title, - iconUrl: chrome.runtime.getURL("image.png"), - - message: message, - buttons: buttons, - contextMessage: chrome.i18n.getMessage('manifest_extensionName'), - items: items, - requireInteraction: !!buttons && !!callback - } - if (!notifyId) notifyId = Math.random().toString(36).substr(2,9); - - chrome.notifications.create(notifyId, opt, function(notificationId){ - if (callback) callbacks[notificationId] = callback; - }); -}; - function updateWindowCoordinates(win, winId, coordinates) { var savedWin = getSetting(win, null); @@ -1372,7 +1106,7 @@ function openVideoWindowUrl(url) chrome.windows.create({url: url, focused: true, type: "popup"}, function (win) { pade.videoWindow = win; - updateWindowCoordinates("videoWindow", pade.videoWindow.id, {width: 1920, height: 1080}); + updateWindowCoordinates("videoWindow", pade.videoWindow.id, {width: 1900, height: 1020}); sendToJabra("offhook"); }); @@ -1589,15 +1323,6 @@ function sendToJabra(message) } } -function clearNotification(room) -{ - chrome.notifications.clear(room, function(wasCleared) - { - console.debug("notification cleared", room, wasCleared); - }); -} - - function removeSetting(name) { localStorage.removeItem("store.settings." + name); @@ -2505,7 +2230,6 @@ function setupBrowserMode(username, password) pade.password = getSetting("password", password); pade.jid = pade.username ? (pade.username + "@" + pade.domain) : pade.domain; pade.displayName = getSetting("displayname", (pade.username ? pade.username : "Anonymous")); - pade.chatWindow = {id: 1}; pade.ofmeetUrl = getSetting("ofmeetUrl", null); diff --git a/pade/classes/public/options/settings.js b/pade/classes/public/options/settings.js index ddc001fc..55d7bd1a 100644 --- a/pade/classes/public/options/settings.js +++ b/pade/classes/public/options/settings.js @@ -1166,8 +1166,7 @@ function doDefaults(background) setDefaultSetting("allowMsgPinning", true); setDefaultSetting("allowMsgReaction", true); setDefaultSetting("useMarkdown", true); - // default is a fewer features - //setDefaultSetting("showToolbarIcons", true); + setDefaultSetting("showToolbarIcons", true); setDefaultSetting("enableNotesTool", true); // most people dont want this //setDefaultSetting("enableRssFeeds", true); diff --git a/pade/classes/public/webpush.js b/pade/classes/public/webpush.js new file mode 100644 index 00000000..47a260c7 --- /dev/null +++ b/pade/classes/public/webpush.js @@ -0,0 +1,153 @@ +var webpush = (function(push) +{ + var hostname, username, password, publicKey; + + function vapidGetPublicKey(host, user, pass) + { + var getUrl = "https://" + host + "/rest/api/restapi/v1/meet/webpush/" + user; + var options = {method: "GET", headers: {"Authorization": "Basic " + btoa(user + ":" + pass), "Accept":"application/json", "Content-Type":"application/json"}}; + + console.debug("vapidGetPublicKey", getUrl, options); + + fetch(getUrl, options).then(function(response) {return response.json()}).then(function(vapid) + { + if (vapid.publicKey) + { + console.debug("vapidGetPublicKey found", vapid); + + publicKey = vapid.publicKey; + hostname = host; + username = user; + password = pass; + + navigator.serviceWorker.register('./serviceworker.js', {scope: './'}).then(initialiseState, initialiseError); + } else { + console.error("no web push, vapid public key not available"); + } + + }).catch(function (err) { + console.error('vapidGetPublicKey error!', err); + }); + } + + function initialiseError(error) + { + console.error("initialiseError", error); + } + + function initialiseState(registration) + { + if (!('showNotification' in ServiceWorkerRegistration.prototype)) { + console.warn('Notifications aren\'t supported.'); + return; + } + + if (Notification.permission === 'denied') { + console.warn('The user has blocked notifications.'); + return; + } + + if (!('PushManager' in window)) { + console.warn('Push messaging isn\'t supported.'); + return; + } + + console.debug("initialiseState", registration); + + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) + { + console.debug("initialiseState ready", serviceWorkerRegistration); + + serviceWorkerRegistration.pushManager.getSubscription().then(function (subscription) + { + console.debug("serviceWorkerRegistration getSubscription", subscription); + + if (!subscription && publicKey) { + subscribe(); + return; + } + + // Keep your server in sync with the latest subscriptionId + sendSubscriptionToServer(subscription); + }) + .catch(function(err) { + console.warn('Error during getSubscription()', err); + }); + }); + } + + function subscribe() + { + console.debug("subscribe", publicKey); + + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) + { + serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: base64UrlToUint8Array(publicKey) + }) + .then(function (subscription) { + return sendSubscriptionToServer(subscription); + }) + .catch(function (e) { + if (Notification.permission === 'denied') { + console.warn('Permission for Notifications was denied'); + } else { + console.error('Unable to subscribe to push.', e); + } + }); + }); + } + + function base64UrlToUint8Array(base64UrlData) + { + const padding = '='.repeat((4 - base64UrlData.length % 4) % 4); + const base64 = (base64UrlData + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = atob(base64); + const buffer = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + buffer[i] = rawData.charCodeAt(i); + } + + return buffer; + } + + function sendSubscriptionToServer(subscription) + { + console.debug("sendSubscriptionToServer", subscription); + + var key = subscription.getKey ? subscription.getKey('p256dh') : ''; + var auth = subscription.getKey ? subscription.getKey('auth') : ''; + + var subscriptionString = JSON.stringify(subscription); // TODO + + console.debug("web push subscription", { + endpoint: subscription.endpoint, + key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '', + auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : '' + }, subscription); + + var resource = chrome.i18n.getMessage('manifest_shortExtensionName').toLowerCase() + "-" + BrowserDetect.browser + BrowserDetect.version + BrowserDetect.OS; + var putUrl = "https://" + hostname + "/rest/api/restapi/v1/meet/webpush/" + username + "/" + resource; + var options = {method: "PUT", body: JSON.stringify(subscription), headers: {"Authorization": "Basic " + btoa(username + ":" + password), "Accept":"application/json", "Content-Type":"application/json"}}; + + return fetch(putUrl, options).then(function(response) { + console.debug("subscribe response", response); + + }).catch(function (err) { + console.error('subscribe error!', err); + }); + } + + push.registerServiceWorker = function(host, username, password) + { + vapidGetPublicKey(host, username, password); + } + + return push; + +}(webpush || {})); \ No newline at end of file diff --git a/pade/pom.xml b/pade/pom.xml index 725aa808..0ed5390d 100644 --- a/pade/pom.xml +++ b/pade/pom.xml @@ -7,7 +7,7 @@ org.igniterealtime.openfire.ofmeet parent - 0.9.13-SNAPSHOT + 1.0.0-SNAPSHOT pade diff --git a/pade/src/java/nl/martijndwars/webpush/Stanza.java b/pade/src/java/nl/martijndwars/webpush/Stanza.java new file mode 100644 index 00000000..d7884a3d --- /dev/null +++ b/pade/src/java/nl/martijndwars/webpush/Stanza.java @@ -0,0 +1,15 @@ +package nl.martijndwars.webpush; + +public class Stanza { + public String msgType; + public String msgFrom; + public String msgBody; + + public Stanza() { } + + public Stanza(String msgType, String msgFrom, String msgBody) { + this.msgType = msgType; + this.msgFrom = msgFrom; + this.msgBody = msgBody; + } +} diff --git a/pade/src/java/nl/martijndwars/webpush/Utils.java b/pade/src/java/nl/martijndwars/webpush/Utils.java index 3a9f922f..e35c89e2 100644 --- a/pade/src/java/nl/martijndwars/webpush/Utils.java +++ b/pade/src/java/nl/martijndwars/webpush/Utils.java @@ -25,7 +25,7 @@ public class Utils { * @return */ public static byte[] savePublicKey(ECPublicKey publicKey) { - return publicKey.getQ().getEncoded(true); + return publicKey.getQ().getEncoded(false); } public static byte[] savePrivateKey(ECPrivateKey privateKey) { diff --git a/pade/src/java/org/igniterealtime/openfire/plugins/pushnotification/PushInterceptor.java b/pade/src/java/org/igniterealtime/openfire/plugins/pushnotification/PushInterceptor.java new file mode 100644 index 00000000..85c9bd74 --- /dev/null +++ b/pade/src/java/org/igniterealtime/openfire/plugins/pushnotification/PushInterceptor.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2019 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.igniterealtime.openfire.plugins.pushnotification; + +import org.dom4j.Element; +import org.dom4j.QName; +import org.jivesoftware.openfire.OfflineMessageListener; +import org.jivesoftware.openfire.XMPPServer; +import org.jivesoftware.openfire.interceptor.PacketInterceptor; +import org.jivesoftware.openfire.interceptor.PacketRejectedException; +import org.jivesoftware.openfire.session.ClientSession; +import org.jivesoftware.openfire.session.Session; +import org.jivesoftware.openfire.user.User; +import org.jivesoftware.openfire.user.UserNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmpp.packet.IQ; +import org.xmpp.packet.JID; +import org.xmpp.packet.Message; +import org.xmpp.packet.Packet; + +import java.sql.SQLException; +import java.util.Map; +import java.util.Set; +import javax.xml.bind.DatatypeConverter; + +import com.google.gson.Gson; +import org.apache.http.HttpResponse; +import nl.martijndwars.webpush.*; +import org.jivesoftware.util.*; + +public class PushInterceptor implements PacketInterceptor, OfflineMessageListener +{ + private static final Logger Log = LoggerFactory.getLogger( PushInterceptor.class ); + + /** + * Invokes the interceptor on the specified packet. The interceptor can either modify + * the packet, or throw a PacketRejectedException to block it from being sent or processed + * (when read).

+ *

+ * An exception can only be thrown when processed is false which means that the read + * packet has not been processed yet or the packet was not sent yet. If the exception is thrown + * with a "read" packet then the sender of the packet will receive an answer with an error. But + * if the exception is thrown with a "sent" packet then nothing will happen.

+ *

+ * Note that for each packet, every interceptor will be called twice: once before processing + * is complete (processing==true) and once after processing is complete. Typically, + * an interceptor will want to ignore one or the other case. + * + * @param packet the packet to take action on. + * @param session the session that received or is sending the packet. + * @param incoming flag that indicates if the packet was read by the server or sent from + * the server. + * @param processed flag that indicates if the action (read/send) was performed. (PRE vs. POST). + * @throws PacketRejectedException if the packet should be prevented from being processed. + */ + @Override + public void interceptPacket( final Packet packet, final Session session, final boolean incoming, final boolean processed ) throws PacketRejectedException + { + if ( incoming ) { + return; + } + + if ( !processed ) { + return; + } + + if ( !(packet instanceof Message)) { + return; + } + + final String body = ((Message) packet).getBody(); + if ( body == null || body.isEmpty() ) + { + return; + } + + if (!(session instanceof ClientSession)) { + return; + } + + final User user; + try + { + user = XMPPServer.getInstance().getUserManager().getUser( ((ClientSession) session).getUsername() ); + } + catch ( UserNotFoundException e ) + { + Log.debug( "Not a recognized user.", e ); + return; + } + + Log.debug( "If user '{}' has push services configured, pushes need to be sent for a message that just arrived.", user ); + tryPushNotification( user, body, packet.getFrom(), ((Message) packet).getType() ); + } + + private void tryPushNotification( User user, String body, JID jid, Message.Type msgtype ) + { + if (XMPPServer.getInstance().getPresenceManager().isAvailable( user )) + { + return; // dont notify if user is online and available. let client handle that + } + + webPush(user, body, jid, msgtype); + } + /** + * Notification message indicating that a message was not stored offline but bounced + * back to the sender. + * + * @param message the message that was bounced. + */ + @Override + public void messageBounced( final Message message ) + {} + + /** + * Notification message indicating that a message was stored offline since the target entity + * was not online at the moment. + * + * @param message the message that was stored offline. + */ + @Override + public void messageStored( final Message message ) + { + if ( message.getBody() == null || message.getBody().isEmpty() ) + { + return; + } + + Log.debug( "Message stored to offline storage. Try to send push notification." ); + final User user; + try + { + user = XMPPServer.getInstance().getUserManager().getUser( message.getTo().getNode() ); + tryPushNotification( user, message.getBody(), message.getFrom(), message.getType() ); + } + catch ( UserNotFoundException e ) + { + Log.error( "Unable to find local user '{}'.", message.getTo().getNode(), e ); + } + } + /** + * Push a payload to a subscribed web push user + * + * + * @param user being pushed to. + * @param publishOptions web push data stored. + * @param body web push payload. + */ + private void webPush( final User user, final String body, JID jid, Message.Type msgtype ) + { + try { + for (String key : user.getProperties().keySet()) + { + if (key.startsWith("webpush.subscribe.")) + { + String publicKey = user.getProperties().get("vapid.public.key"); + String privateKey = user.getProperties().get("vapid.private.key"); + + if (publicKey == null) publicKey = JiveGlobals.getProperty("vapid.public.key", null); + if (privateKey == null) privateKey = JiveGlobals.getProperty("vapid.private.key", null); + + if (publicKey != null && privateKey != null) + { + PushService pushService = new PushService() + .setPublicKey(publicKey) + .setPrivateKey(privateKey) + .setSubject("mailto:admin@" + XMPPServer.getInstance().getServerInfo().getXMPPDomain()); + + Subscription subscription = new Gson().fromJson(user.getProperties().get(key), Subscription.class); + Stanza stanza = new Stanza(msgtype == Message.Type.chat ? "chat" : "groupchat", jid.asBareJID().toString(), body); + Notification notification = new Notification(subscription, (new Gson().toJson(stanza)).toString()); + HttpResponse response = pushService.send(notification); + int statusCode = response.getStatusLine().getStatusCode(); + + Log.debug( "For user '{}', Web push notification response '{}'", user.toString(), response.getStatusLine().getStatusCode() ); + } + } + } + } catch (Exception e) { + Log.warn( "An exception occurred while trying send a web push for user '{}'.", new Object[] { user, e } ); + } + } +} diff --git a/pade/src/java/uk/ifsoft/openfire/plugins/pade/PadePlugin.java b/pade/src/java/uk/ifsoft/openfire/plugins/pade/PadePlugin.java index 30a6ff90..41eb6b07 100644 --- a/pade/src/java/uk/ifsoft/openfire/plugins/pade/PadePlugin.java +++ b/pade/src/java/uk/ifsoft/openfire/plugins/pade/PadePlugin.java @@ -1,5 +1,7 @@ package uk.ifsoft.openfire.plugins.pade; +import org.jivesoftware.openfire.OfflineMessageStrategy; +import org.jivesoftware.openfire.interceptor.InterceptorManager; import org.jivesoftware.openfire.http.HttpBindManager; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.container.Plugin; @@ -45,6 +47,7 @@ import waffle.servlet.WaffleInfoServlet; import org.xmpp.packet.*; import org.dom4j.Element; +import org.igniterealtime.openfire.plugins.pushnotification.PushInterceptor; public class PadePlugin implements Plugin, MUCEventListener @@ -55,6 +58,7 @@ public class PadePlugin implements Plugin, MUCEventListener private WebAppContext contextPublic; private WebAppContext contextPrivate; private WebAppContext contextWinSSO; + private PushInterceptor interceptor; /** * Initializes the plugin. @@ -67,6 +71,10 @@ public void initializePlugin( final PluginManager manager, final File pluginDire { Log.info("start pade server"); + interceptor = new PushInterceptor(); + InterceptorManager.getInstance().addInterceptor( interceptor ); + OfflineMessageStrategy.addListener( interceptor ); + contextRest = new ServletContextHandler(null, "/rest", ServletContextHandler.SESSIONS); contextRest.setClassLoader(this.getClass().getClassLoader()); contextRest.addServlet(new ServletHolder(new JerseyWrapper()), "/api/*"); @@ -165,6 +173,9 @@ public void destroyPlugin() { MUCEventDispatcher.removeListener(this); } + + OfflineMessageStrategy.removeListener( interceptor ); + InterceptorManager.getInstance().removeInterceptor( interceptor ); } private static final SecurityHandler basicAuth(String realm) { @@ -272,19 +283,35 @@ public void messageReceived(JID roomJID, JID user, String nickname, Message mess { MUCRoom room = mucService.getChatRoom(roomJID.getNode()); - for (JID jid : room.getOwners()) - { - notifyRoomSubscribers(jid, room, roomJID); - } - - for (JID jid : room.getAdmins()) + if (room != null) { - notifyRoomSubscribers(jid, room, roomJID); - } - - for (JID jid : room.getMembers()) - { - notifyRoomSubscribers(jid, room, roomJID); + for (JID jid : room.getOwners()) + { + Log.debug("notifyRoomSubscribers owners " + jid + " " + roomJID); + notifyRoomSubscribers(jid, room, roomJID); + } + + for (JID jid : room.getAdmins()) + { + Log.debug("notifyRoomSubscribers admins " + jid + " " + roomJID); + notifyRoomSubscribers(jid, room, roomJID); + } + + for (JID jid : room.getMembers()) + { + Log.debug("notifyRoomSubscribers members " + jid + " " + roomJID); + notifyRoomSubscribers(jid, room, roomJID); + } + + for (MUCRole role : room.getModerators()) + { + Log.debug("notifyRoomSubscribers moderators " + role.getUserAddress() + " " + roomJID); + } + + for (MUCRole role : room.getParticipants()) + { + Log.debug("notifyRoomSubscribers participants " + role.getUserAddress() + " " + roomJID); + } } } @@ -307,8 +334,6 @@ public void privateMessageRecieved(JID a, JID b, Message message) private void notifyRoomSubscribers(JID subscriberJID, MUCRoom room, JID roomJID) { - Log.debug("notifyRoomSubscribers " + subscriberJID + " " + roomJID); - try { if (GroupJID.isGroup(subscriberJID)) { Group group = GroupManager.getInstance().getGroup(subscriberJID); @@ -330,24 +355,24 @@ private void notifyRoomActivity(JID subscriberJID, MUCRoom room, JID roomJID) if (room.getAffiliation(subscriberJID) != MUCRole.Affiliation.none) { Log.debug("notifyRoomActivity checking " + subscriberJID + " " + roomJID); - boolean inRoom = true; + boolean inRoom = false; try { - List roles = room.getOccupantsByBareJID(subscriberJID); - - if (roles.size() > 1 && roles.get(0).getPresence().isAvailable() == false) + for (MUCRole role : room.getOccupants()) { - inRoom = false; + if (role.getUserAddress().asBareJID().toString().equals(subscriberJID.toString())) inRoom = true; } } catch (Exception e) { inRoom = false; + Log.error("notifyRoomActivity error", e); } Log.debug("notifyRoomActivity confirmed " + subscriberJID + " " + roomJID + " " + inRoom); - if (!inRoom) + if (!inRoom && XMPPServer.getInstance().getRoutingTable().getRoutes(subscriberJID, null).size() > 0) { + Log.debug("notifyRoomActivity notifying " + subscriberJID + " " + roomJID); Message message = new Message(); message.setFrom(roomJID); message.setTo(subscriberJID); diff --git a/pom.xml b/pom.xml index 907ef7ea..089cb1e2 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ org.igniterealtime.openfire.ofmeet parent - 0.9.13-SNAPSHOT + 1.0.0-SNAPSHOT pom diff --git a/videobridge/pom.xml b/videobridge/pom.xml index fc2096e1..350a1c7e 100644 --- a/videobridge/pom.xml +++ b/videobridge/pom.xml @@ -23,7 +23,7 @@ org.igniterealtime.openfire.ofmeet parent - 0.9.13-SNAPSHOT + 1.0.0-SNAPSHOT videobridge