Selaa lähdekoodia

Add a plugin for XEP-0357

Currently only allows enabling an "App Service", based on pass-in
configuration values.
JC Brand 7 vuotta sitten
vanhempi
commit
7ee71b0132
9 muutettua tiedostoa jossa 363 lisäystä ja 44 poistoa
  1. 1 0
      .eslintrc.json
  2. 1 0
      CHANGES.md
  3. 102 11
      dist/converse.js
  4. 56 23
      docs/source/configuration.rst
  5. 119 0
      spec/push.js
  6. 12 10
      src/converse-core.js
  7. 70 0
      src/converse-push.js
  8. 1 0
      src/converse.js
  9. 1 0
      tests/runner.js

+ 1 - 0
.eslintrc.json

@@ -9,6 +9,7 @@
     "plugins": ["lodash"],
     "extends": ["eslint:recommended", "plugin:lodash/canonical"],
     "globals": {
+        "Promise": true,
         "converse": true,
         "window": true,
         "sinon": true,

+ 1 - 0
CHANGES.md

@@ -19,6 +19,7 @@
 - Add a checkbox to indicate whether a trusted device is being used or not.
   If the device is not trusted, sessionStorage is used and all user data is deleted from the browser cache upon logout.
   If the device is trusted, localStorage is used and user data is cached indefinitely.
+- Initial support for XEP-0357 Push Notifications, specifically registering an "App Server".
 
 ### Bugfixes
 

+ 102 - 11
dist/converse.js

@@ -64922,11 +64922,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
 })(void 0, function (sizzle, Promise, _, f, polyfill, i18n, u, moment, Strophe, pluggable, Backbone) {
-  /* Cannot use this due to Safari bug.
-   * See https://github.com/jcbrand/converse.js/issues/196
-   */
-  // "use strict";
-  // Strophe globals
+  "use strict"; // Strophe globals
+
   const _Strophe = Strophe,
         $build = _Strophe.$build,
         $iq = _Strophe.$iq,
@@ -64974,7 +64971,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
   _.extend(_converse, Backbone.Events); // Core plugins are whitelisted automatically
 
 
-  _converse.core_plugins = ['converse-bookmarks', 'converse-chatboxes', 'converse-chatview', 'converse-caps', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-embedded', 'converse-fullscreen', 'converse-headline', 'converse-mam', 'converse-message-view', 'converse-minimize', 'converse-modal', 'converse-muc', 'converse-muc-views', 'converse-notification', 'converse-otr', 'converse-ping', 'converse-profile', 'converse-register', 'converse-roomslist', 'converse-roster', 'converse-rosterview', 'converse-singleton', 'converse-spoilers', 'converse-vcard']; // Make converse pluggable
+  _converse.core_plugins = ['converse-bookmarks', 'converse-caps', 'converse-chatboxes', 'converse-chatview', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-embedded', 'converse-fullscreen', 'converse-headline', 'converse-mam', 'converse-message-view', 'converse-minimize', 'converse-modal', 'converse-muc', 'converse-muc-views', 'converse-notification', 'converse-ping', 'converse-profile', 'converse-push', 'converse-register', 'converse-roomslist', 'converse-roster', 'converse-rosterview', 'converse-singleton', 'converse-spoilers', 'converse-vcard']; // Make converse pluggable
 
   pluggable.enable(_converse, '_converse', 'pluggable'); // Module-level constants
 
@@ -65004,6 +65001,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
   _converse.LOGOUT = "logout";
   _converse.OPENED = 'opened';
   _converse.PREBIND = "prebind";
+  _converse.IQ_TIMEOUT = 30000;
   _converse.CONNECTION_STATUS = {
     0: 'ERROR',
     1: 'CONNECTING',
@@ -65173,8 +65171,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
   _converse.router = new Backbone.Router();
 
   _converse.initialize = function (settings, callback) {
-    "use strict";
-
     settings = !_.isUndefined(settings) ? settings : {};
     const init_promise = u.getResolveablePromise();
 
@@ -65564,7 +65560,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
       const id = b64_sha1('converse.bosh-session');
       _converse.session.id = id; // Appears to be necessary for backbone.browserStorage
 
-      _converse.session.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
+      _converse.session.browserStorage = new Backbone.BrowserStorage.session(id);
 
       _converse.session.fetch();
 
@@ -65682,7 +65678,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           _converse.log('An error occured while trying to enable message carbons.', Strophe.LogLevel.ERROR);
         } else {
           this.session.save({
-            carbons_enabled: true
+            'carbons_enabled': true
           });
 
           _converse.log('Message carbons have been enabled.');
@@ -66272,6 +66268,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
     'send'(stanza) {
       _converse.connection.send(stanza);
+    },
+
+    'sendIQ'(stanza) {
+      return new Promise((resolve, reject) => {
+        _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
+      });
     }
 
   }; // The public API
@@ -73287,6 +73289,94 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
 /***/ }),
 
+/***/ "./src/converse-push.js":
+/*!******************************!*\
+  !*** ./src/converse-push.js ***!
+  \******************************/
+/*! no static exports found */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;
+
+// Converse.js
+// https://conversejs.org
+//
+// Copyright (c) 2013-2018, the Converse.js developers
+// Licensed under the Mozilla Public License (MPLv2)
+
+/* This is a Converse.js plugin which add support for registering
+ * an "App Server" as defined in  XEP-0357
+ */
+(function (root, factory) {
+  !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
+				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
+				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+})(void 0, function (converse) {
+  "use strict";
+
+  const _converse$env = converse.env,
+        Strophe = _converse$env.Strophe,
+        $iq = _converse$env.$iq;
+  Strophe.addNamespace('PUSH', 'urn:xmpp:push:0');
+  converse.plugins.add('converse-push', {
+    initialize() {
+      /* The initialize function gets called as soon as the plugin is
+       * loaded by converse.js's plugin machinery.
+       */
+      const _converse = this._converse,
+            __ = _converse.__;
+
+      _converse.api.settings.update({
+        'push_service': undefined,
+        'push_service_node': undefined,
+        'push_service_secret': undefined
+      });
+
+      function enablePush() {
+        if (_converse.session.get('push_enabled')) {
+          return;
+        }
+
+        if (_converse.push_service && _converse.push_service_node) {
+          Promise.all([_converse.api.disco.getIdentity('pubsub', 'push', _converse.push_service), _converse.api.disco.supports(Strophe.NS.PUSH, _converse.push_service)]).then(() => _converse.api.disco.supports(Strophe.NS.PUSH, _converse.bare_jid)).then(() => {
+            const stanza = $iq({
+              'type': 'set'
+            }).c('enable', {
+              'xmlns': Strophe.NS.PUSH,
+              'jid': _converse.push_service,
+              'node': _converse.push_service_node
+            });
+
+            if (_converse.push_service_secret) {
+              stanza.c('x', {
+                'xmlns': Strophe.NS.XFORM,
+                'type': 'submit'
+              }).c('field', {
+                'var': 'FORM_TYPE'
+              }).c('value').t(`${Strophe.NS.PUBSUB}#publish-options`).up().up().c('field', {
+                'var': 'secret'
+              }).c('value').t(_converse.push_service_secret);
+            }
+
+            _converse.api.sendIQ(stanza).then(() => _converse.session.set('push_enabled', true)).catch(e => {
+              _converse.log(`Could not enable push service for ${_converse.push_service}`, Strophe.LogLevel.ERROR);
+
+              _converse.log(e, Strophe.LogLevel.ERROR);
+            });
+          });
+        }
+      }
+
+      _converse.api.listen.on('statusInitialized', enablePush);
+    }
+
+  });
+});
+
+/***/ }),
+
 /***/ "./src/converse-register.js":
 /*!**********************************!*\
   !*** ./src/converse-register.js ***!
@@ -76844,7 +76934,8 @@ if (true) {
   __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), // Renders standalone chat boxes for single user chat
   __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js"), // The control box
   __webpack_require__(/*! converse-dragresize */ "./src/converse-dragresize.js"), // Allows chat boxes to be resized by dragging them
-  __webpack_require__(/*! converse-embedded */ "./src/converse-embedded.js"), __webpack_require__(/*! converse-fullscreen */ "./src/converse-fullscreen.js"), __webpack_require__(/*! converse-headline */ "./src/converse-headline.js"), // Support for headline messages
+  __webpack_require__(/*! converse-embedded */ "./src/converse-embedded.js"), __webpack_require__(/*! converse-fullscreen */ "./src/converse-fullscreen.js"), __webpack_require__(/*! converse-push */ "./src/converse-push.js"), // XEP-0357 Push Notifications
+  __webpack_require__(/*! converse-headline */ "./src/converse-headline.js"), // Support for headline messages
   __webpack_require__(/*! converse-mam */ "./src/converse-mam.js"), // XEP-0313 Message Archive Management
   __webpack_require__(/*! converse-minimize */ "./src/converse-minimize.js"), // Allows chat boxes to be minimized
   __webpack_require__(/*! converse-muc */ "./src/converse-muc.js"), // XEP-0045 Multi-user chat

+ 56 - 23
docs/source/configuration.rst

@@ -7,10 +7,10 @@ Configuration
 =============
 
 The included minified JavaScript and CSS files can be used for demoing or testing, but
-you'll want to configure *Converse.js* to suit your needs before you deploy it
+you'll want to configure *Converse* to suit your needs before you deploy it
 on your website.
 
-*Converse.js* is passed its configuration settings when you call its *initialize* method.
+*Converse* is passed its configuration settings when you call its *initialize* method.
 
 You'll most likely want to call the *initialize* method in your HTML page. For
 an example of how this is done, please see the bottom of the *./index.html* page.
@@ -18,7 +18,7 @@ an example of how this is done, please see the bottom of the *./index.html* page
 Please refer to the `Configuration settings`_ section below for info on
 all the available configuration settings.
 
-After you have configured *Converse.js*, you'll have to regenerate the minified
+After you have configured *Converse*, you'll have to regenerate the minified
 JavaScript file so that it will include the new settings. Please refer to the
 :ref:`minification` section for more info on how to do this.
 
@@ -83,7 +83,7 @@ requiring them to log in manually.
 When a BOSH session is initially created, you'll receive three tokens.
 A JID (jabber ID), SID (session ID) and RID (Request ID).
 
-Converse.js needs these tokens in order to attach to that same session.
+Converse needs these tokens in order to attach to that same session.
 
 There are two complementary configuration settings to ``prebind``.
 They are :ref:`keepalive` and `prebind_url`_.
@@ -200,7 +200,7 @@ allow_public_bookmarks
 Some XMPP servers don't support private PEP/PubSub nodes, as required for
 private bookmarks and outlined in `XEP-0223 <https://xmpp.org/extensions/xep-0223.html>`_.
 
-Even though Converse.js asks for the bookmarks to be kept private (via the
+Even though Converse asks for the bookmarks to be kept private (via the
 `<publish-options>` XML node), the server simply ignores the privacy settings
 and publishes the node contents under the default privacy setting, which makes
 the information available to all roster contacts.
@@ -382,7 +382,7 @@ A list of plugin names that are blacklisted and will therefore not be
 initialized once ``converse.initialize`` is called, even if the same plugin is
 whitelisted.
 
-From Converse.js 3.0 onwards most of the API is available only to plugins and
+From Converse 3.0 onwards most of the API is available only to plugins and
 all plugins need to be whitelisted first.
 
 The usecase for blacklisting is generally to disable removed core plugins
@@ -495,7 +495,7 @@ connection_options
 * Default:  ``{}``
 * Type:  Object
 
-Converse.js relies on `Strophe.js <http://strophe.im>`_ to establish and
+Converse relies on `Strophe.js <http://strophe.im>`_ to establish and
 maintain a connection to the XMPP server.
 
 This option allows you to pass a map of configuration options to be passed into
@@ -725,7 +725,7 @@ The translations for that locale must be available in JSON format at the
 
 If an explicit locale is specified via the ``i18n`` setting and the
 translations for that locale are not found at the `locales_url``, then 
-then Converse.js will fall back to trying to determine the browser's language
+then Converse will fall back to trying to determine the browser's language
 and fetching those translations, or if that fails the default English texts
 will be used.
 
@@ -747,7 +747,7 @@ keepalive
 
 * Default:    ``true``
 
-Determines whether Converse.js will maintain the chat session across page
+Determines whether Converse will maintain the chat session across page
 loads.
 
 This setting should also be used in conjunction with ``authentication`` set to `prebind`_.
@@ -785,7 +785,7 @@ locales
         'ru', 'uk', 'zh'
     ]
 
-This setting restricts the locales that are supported by Converse.js and
+This setting restricts the locales that are supported by Converse and
 therefore what may be given as value for the :ref:`i18n` option.
 
 Any other locales will be ignored.
@@ -800,7 +800,7 @@ locales_url
 
 * Default: ``/locale/{{{locale}}}/LC_MESSAGES/converse.json``,
 
-The URL from where Converse.js should fetch translation JSON.
+The URL from where Converse should fetch translation JSON.
 
 The three curly braces ``{{{ }}}`` are
 `Mustache <https://github.com/janl/mustache.js#readme>`_-style
@@ -812,7 +812,7 @@ The variable being interpolated via the curly braces is ``locale``, which is
 the value passed in to the `i18n`_ setting, or the browser's locale or the
 default local or `en` (resolved in that order).
 
-From version 3.3.0, Converse.js no longer bundles all translations into its
+From version 3.3.0, Converse no longer bundles all translations into its
 final build file. Instead, only the relevant translations are fetched at
 runtime.
 
@@ -953,7 +953,7 @@ muc_show_join_leave
 
 * Default; ``true``
 
-Determines whether Converse.js will show info messages inside a chatroom
+Determines whether Converse will show info messages inside a chatroom
 whenever a user joins or leaves it.
 
 nickname
@@ -1007,7 +1007,7 @@ play_sounds
 Plays a notification sound when you receive a personal message or when your
 nickname is mentioned in a chatroom.
 
-Inside the ``./sounds`` directory of the Converse.js repo you'll see MP3 and Ogg
+Inside the ``./sounds`` directory of the Converse repo you'll see MP3 and Ogg
 formatted sound files. We need both, because neither format is supported by all browsers.
 
 You can set the URL where the sound files are hosted with the `sounds_path`_
@@ -1047,12 +1047,12 @@ priority
 * Type:     Number
 
 Determines the priority used for presence stanzas sent out from this resource
-(i.e. this instance of Converse.js).
+(i.e. this instance of Converse).
 
 The priority of a given XMPP chat client determines the importance of its presence
 stanzas in relation to stanzas received from other clients of the same user.
 
-In Converse.js, the indicated chat status of a roster contact will be taken from the
+In Converse, the indicated chat status of a roster contact will be taken from the
 presence stanza (and associated resource) with the highest priority.
 
 If multiple resources have the same top priority, then the chat status will be
@@ -1068,6 +1068,39 @@ providers_link
 The hyperlink on the registration form which points to a directory of public
 XMPP servers.
 
+push_service
+------------
+
+* Default: ``undefined``
+
+This option allows you to specify a URI for the push notifications service
+(called an "App Server" by `XEP-0357 <https://xmpp.org/extensions/xep-0357.html>`_).
+
+If provided, together with a `push_service_node`_, then Converse will instruct
+the user's XMPP server to send push notificatiosn to that URI.
+
+push_service_node
+-----------------
+
+* Default: ``undefined``
+
+This is the PubSub node of the push notifications service (aka "App Server") specified with the
+`push_service`_ setting.
+
+Push notifications will be sent to this node. If this value is not set, then
+push notifications won't be sent out.
+
+push_service_secret
+-------------------
+
+* Default: ``undefined``
+
+Some push notification services (aka "App Servers") require a secret token to
+be used when sending out notifications.
+
+This setting enables you to provide such a secret to Converse which will
+forward it to your XMPP server to be included in push notifications.
+
 root
 ----
 
@@ -1108,7 +1141,7 @@ configured.
 
 .. note::
     It's currently not possible to use converse.js to assign contacts to groups.
-    Converse.js can only show users and groups that were previously configured
+    Converse can only show users and groups that were previously configured
     elsewhere.
 
 show_chatstate_notifications
@@ -1262,10 +1295,10 @@ loaded), then an error will be raised.
 
 Otherwise a message will simply be logged and the override instruction ignored.
 
-The Converse.js plugins architecture can have an :ref:`dependencies`
+The Converse plugins architecture can have an :ref:`dependencies`
 plugin attribute. This enables you to specify an array of other plugins which
 this one depends on.
-Converse.js (more specifically, `pluggable.js <https://jcbrand.github.io/pluggable.js/>`_)
+Converse (more specifically, `pluggable.js <https://jcbrand.github.io/pluggable.js/>`_)
 will first load these dependencies before executing the plugin's overrides and
 calling its ``initialize`` method.
 
@@ -1302,7 +1335,7 @@ This setting determines whether the default value of the "This is a trusted devi
 When the current device is not trusted, then localStorage and sessionStorage
 will be cleared when the user logs out, thereby removing all cached data.
 
-Clearing the cache in this way makes Converse.js much slower when the user logs
+Clearing the cache in this way makes Converse much slower when the user logs
 in again, because all data needs to be fetch anew.
 
 See also `storage`_.
@@ -1323,7 +1356,7 @@ use_otr_by_default
 
 * Default:  ``false``
 
-If set to ``true``, Converse.js will automatically try to initiate an OTR (off-the-record)
+If set to ``true``, Converse will automatically try to initiate an OTR (off-the-record)
 encrypted chat session every time you open a chatbox.
 
 visible_toolbar_buttons
@@ -1382,7 +1415,7 @@ support.
     configuration setting).
 
 .. note::
-    Converse.js does not yet support "keepalive" with websockets.
+    Converse does not yet support "keepalive" with websockets.
 
 .. _`view_mode`:
 
@@ -1448,7 +1481,7 @@ whitelisted_plugins
 A list of plugin names that are whitelisted and will therefore be
 initialized once ``converse.initialize`` is called.
 
-From Converse.js 3.0 onwards most of the API is available only to plugins and
+From Converse 3.0 onwards most of the API is available only to plugins and
 all plugins need to be whitelisted first.
 
 This is done to prevent malicious scripts from using the API to trick users or

+ 119 - 0
spec/push.js

@@ -0,0 +1,119 @@
+(function (root, factory) {
+    define(["jasmine", "mock", "test-utils"], factory);
+} (this, function (jasmine, mock, test_utils) {
+    "use strict";
+    var $iq = converse.env.$iq;
+    var Strophe = converse.env.Strophe;
+    var _ = converse.env._;
+
+    describe("XEP-0357 Push Notifications", function () {
+
+        it("can be enabled by specifying a push_service and push_service_node",
+            mock.initConverseWithPromises(null, 
+                ['rosterGroupsFetched'], {
+                    'push_service': 'push-5@client.example',
+                    'push_service_node': 'yxs32uqsflafdk3iuqo' 
+                }, function (done, _converse) {
+
+            const IQ_stanzas = _converse.connection.IQ_stanzas;
+            let stanza;
+
+            expect(_converse.push_service).toBe('push-5@client.example');
+            expect(_converse.push_service_node).toBe('yxs32uqsflafdk3iuqo');
+            expect(_converse.session.get('push_enabled')).toBeFalsy();
+
+            test_utils.waitUntilDiscoConfirmed(
+                _converse, _converse.push_service,
+                [{'category': 'pubsub', 'type':'push'}],
+                ['urn:xmpp:push:0'], [], 'info')
+            .then(() => test_utils.waitUntilDiscoConfirmed(
+                    _converse,
+                    _converse.bare_jid,
+                    [{'category': 'account', 'type':'registered'}],
+                    ['urn:xmpp:push:0'], [], 'info'))
+            .then(() => {
+                return test_utils.waitUntil(() => {
+                    const node = _.filter(IQ_stanzas, function (iq) {
+                        return iq.nodeTree.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]');
+                    }).pop();
+                    if (node) {
+                        stanza = node.nodeTree;
+                        return true;
+                    }
+                })
+            }).then(() => {
+                expect(stanza.outerHTML).toEqual(
+                    `<iq type="set" xmlns="jabber:client" id="${stanza.getAttribute('id')}">`+
+                        '<enable xmlns="urn:xmpp:push:0" jid="push-5@client.example" node="yxs32uqsflafdk3iuqo"/>'+
+                    '</iq>'
+                )
+                _converse.connection._dataRecv(test_utils.createRequest($iq({
+                    'to': _converse.connection.jid,
+                    'type': 'result',
+                    'id': stanza.getAttribute('id')
+                })));
+                return test_utils.waitUntil(() => _converse.session.get('push_enabled'))
+            }).then(() => {
+                done();
+            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+        }));
+
+
+        it("can require a secret token to be included",
+            mock.initConverseWithPromises(null, 
+                ['rosterGroupsFetched'], {
+                    'push_service': 'push-5@client.example',
+                    'push_service_node': 'yxs32uqsflafdk3iuqo',
+                    'push_service_secret': 'eruio234vzxc2kla-91'
+                }, function (done, _converse) {
+
+            const IQ_stanzas = _converse.connection.IQ_stanzas;
+            let stanza;
+
+            expect(_converse.push_service).toBe('push-5@client.example');
+            expect(_converse.push_service_node).toBe('yxs32uqsflafdk3iuqo');
+            expect(_converse.push_service_secret).toBe('eruio234vzxc2kla-91');
+            expect(_converse.session.get('push_enabled')).toBeFalsy();
+
+            test_utils.waitUntilDiscoConfirmed(
+                _converse, _converse.push_service,
+                [{'category': 'pubsub', 'type':'push'}],
+                ['urn:xmpp:push:0'], [], 'info')
+            .then(() => test_utils.waitUntilDiscoConfirmed(
+                    _converse,
+                    _converse.bare_jid,
+                    [{'category': 'account', 'type':'registered'}],
+                    ['urn:xmpp:push:0'], [], 'info'))
+            .then(() => {
+                return test_utils.waitUntil(() => {
+                    const node = _.filter(IQ_stanzas, function (iq) {
+                        return iq.nodeTree.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]');
+                    }).pop();
+                    if (node) {
+                        stanza = node.nodeTree;
+                        return true;
+                    }
+                })
+            }).then(() => {
+                expect(stanza.outerHTML).toEqual(
+                    `<iq type="set" xmlns="jabber:client" id="${stanza.getAttribute('id')}">`+
+                        '<enable xmlns="urn:xmpp:push:0" jid="push-5@client.example" node="yxs32uqsflafdk3iuqo">'+
+                            '<x xmlns="jabber:x:data" type="submit">'+
+                                '<field var="FORM_TYPE"><value>http://jabber.org/protocol/pubsub#publish-options</value></field>'+
+                                '<field var="secret"><value>eruio234vzxc2kla-91</value></field>'+
+                            '</x>'+
+                        '</enable>'+
+                    '</iq>'
+                )
+                _converse.connection._dataRecv(test_utils.createRequest($iq({
+                    'to': _converse.connection.jid,
+                    'type': 'result',
+                    'id': stanza.getAttribute('id')
+                })));
+                return test_utils.waitUntil(() => _converse.session.get('push_enabled'))
+            }).then(() => {
+                done();
+            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+        }));
+    });
+}));

+ 12 - 10
src/converse-core.js

@@ -20,11 +20,7 @@
             "backbone.browserStorage"
     ], factory);
 }(this, function (sizzle, Promise, _, f, polyfill, i18n, u, moment, Strophe, pluggable, Backbone) {
-
-    /* Cannot use this due to Safari bug.
-     * See https://github.com/jcbrand/converse.js/issues/196
-     */
-    // "use strict";
+    "use strict";
 
     // Strophe globals
     const { $build, $iq, $msg, $pres } = Strophe;
@@ -72,9 +68,9 @@
     // Core plugins are whitelisted automatically
     _converse.core_plugins = [
         'converse-bookmarks',
+        'converse-caps',
         'converse-chatboxes',
         'converse-chatview',
-        'converse-caps',
         'converse-controlbox',
         'converse-core',
         'converse-disco',
@@ -89,9 +85,9 @@
         'converse-muc',
         'converse-muc-views',
         'converse-notification',
-        'converse-otr',
         'converse-ping',
         'converse-profile',
+        'converse-push',
         'converse-register',
         'converse-roomslist',
         'converse-roster',
@@ -131,6 +127,8 @@
     _converse.OPENED = 'opened';
     _converse.PREBIND = "prebind";
 
+    _converse.IQ_TIMEOUT = 30000;
+
     _converse.CONNECTION_STATUS = {
         0: 'ERROR',
         1: 'CONNECTING',
@@ -298,7 +296,6 @@
 
 
     _converse.initialize = function (settings, callback) {
-        "use strict";
         settings = !_.isUndefined(settings) ? settings : {};
         const init_promise = u.getResolveablePromise();
 
@@ -638,7 +635,7 @@
             _converse.session = new Backbone.Model();
             const id = b64_sha1('converse.bosh-session');
             _converse.session.id = id; // Appears to be necessary for backbone.browserStorage
-            _converse.session.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
+            _converse.session.browserStorage = new Backbone.BrowserStorage.session(id);
             _converse.session.fetch();
             _converse.emit('sessionInitialized');
         };
@@ -739,7 +736,7 @@
                         'An error occured while trying to enable message carbons.',
                         Strophe.LogLevel.ERROR);
                 } else {
-                    this.session.save({carbons_enabled: true});
+                    this.session.save({'carbons_enabled': true});
                     _converse.log('Message carbons have been enabled.');
                 }
             }, null, "iq", null, "enablecarbons");
@@ -1289,6 +1286,11 @@
         'send' (stanza) {
             _converse.connection.send(stanza);
         },
+        'sendIQ' (stanza) {
+            return new Promise((resolve, reject) => {
+                _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
+            });
+        }
     };
 
     // The public API

+ 70 - 0
src/converse-push.js

@@ -0,0 +1,70 @@
+// Converse.js
+// https://conversejs.org
+//
+// Copyright (c) 2013-2018, the Converse.js developers
+// Licensed under the Mozilla Public License (MPLv2)
+
+/* This is a Converse.js plugin which add support for registering
+ * an "App Server" as defined in  XEP-0357
+ */
+(function (root, factory) {
+    define(["converse-core"], factory);
+}(this, function (converse) {
+    "use strict";
+    const { Strophe, $iq } = converse.env;
+
+
+    Strophe.addNamespace('PUSH', 'urn:xmpp:push:0');
+
+    
+    converse.plugins.add('converse-push', {
+
+        initialize () {
+            /* The initialize function gets called as soon as the plugin is
+             * loaded by converse.js's plugin machinery.
+             */
+            const { _converse } = this,
+                  { __ } = _converse;
+
+            _converse.api.settings.update({
+                'push_service': undefined,
+                'push_service_node': undefined,
+                'push_service_secret': undefined
+            });
+
+            function enablePush() {
+                if (_converse.session.get('push_enabled')) {
+                    return;
+                }
+                if (_converse.push_service && _converse.push_service_node) {
+                    Promise.all([
+                        _converse.api.disco.getIdentity('pubsub', 'push', _converse.push_service),
+                        _converse.api.disco.supports(Strophe.NS.PUSH, _converse.push_service)
+                    ]).then(() => _converse.api.disco.supports(Strophe.NS.PUSH, _converse.bare_jid))
+                      .then(() => {
+                        const stanza = $iq({'type': 'set'})
+                            .c('enable', {
+                                'xmlns': Strophe.NS.PUSH,
+                                'jid': _converse.push_service,
+                                'node': _converse.push_service_node
+                            });
+                          if (_converse.push_service_secret) {
+                              stanza.c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
+                                .c('field', {'var': 'FORM_TYPE'})
+                                    .c('value').t(`${Strophe.NS.PUBSUB}#publish-options`).up().up()
+                                .c('field', {'var': 'secret'})
+                                    .c('value').t(_converse.push_service_secret);
+                          }
+                        _converse.api.sendIQ(stanza)
+                          .then(() => _converse.session.set('push_enabled', true))
+                          .catch((e) => {
+                              _converse.log(`Could not enable push service for ${_converse.push_service}`, Strophe.LogLevel.ERROR);
+                              _converse.log(e, Strophe.LogLevel.ERROR);
+                          });
+                    });
+                }
+            }
+            _converse.api.listen.on('statusInitialized', enablePush);
+        }
+    });
+}));

+ 1 - 0
src/converse.js

@@ -14,6 +14,7 @@ if (typeof define !== 'undefined') {
         "converse-dragresize",      // Allows chat boxes to be resized by dragging them
         "converse-embedded",
         "converse-fullscreen",
+        "converse-push",            // XEP-0357 Push Notifications
         "converse-headline",        // Support for headline messages
         "converse-mam",             // XEP-0313 Message Archive Management
         "converse-minimize",        // Allows chat boxes to be minimized

+ 1 - 0
tests/runner.js

@@ -191,6 +191,7 @@ var specs = [
     "spec/presence",
     "spec/eventemitter",
     "spec/ping",
+    "spec/push",
     "spec/xmppstatus",
     "spec/mam",
     // "spec/otr",