Commit 7b11d855 authored by JC Brand's avatar JC Brand

Add support for XEP-0198 Stream Management

- New plugin `converse-smacks`
- New config option `enable_smacks`
- Rename session cache id from `converse.bosh-session` to `converse.session`
- Refactor logout and login as consistently used api methods
- Refactor session cache to store per JID

Fixes #316
parent a46ee4df
......@@ -4,7 +4,7 @@ cache:
directories:
- node_modules
addons:
chrome: unstable
chrome: stable
node_js:
- "10"
install: make stamp-npm
......
......@@ -15,15 +15,15 @@
- Message deduplication bugfixes and improvements
- Continuously retry (in 2s intervals) to fetch login credentials (via [credentials_url](https://conversejs.org/docs/html/configuration.html#credentials-url)) in case of failure
- Replace `moment` with [DayJS](https://github.com/iamkun/dayjs).
- New API method [\_converse.api.disco.features.get](https://conversejs.org/docs/html/api/-_converse.api.disco.features.html#.get)
- New config setting [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status)
- New config option [enable_smacks](https://conversejs.org/docs/html/configuration.html#enable-smacks).
- New config option [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status)
- New config option [singleton](https://conversejs.org/docs/html/configuration.html#singleton).
By setting this option to `false` and `view_mode` to `'embedded'`, it's now possible to
"embed" the full app and not just a single chat. To embed just a single chat, it's now
necessary to explicitly set `singleton` to `true`.
- New event: `chatBoxBlurred`.
- New event: [chatBoxBlurred](https://conversejs.org/docs/html/api/-_converse.html#event:chatBoxBlurred)
- New event: [chatReconnected](https://conversejs.org/docs/html/api/-_converse.html#event:chatReconnected)
- #316: Add support for XEP-0198 Stream Management
- #1296: `embedded` view mode shows `chatbox-navback` arrow in header
- #1465: When highlighting a roster contact, they're incorrectly shown as online
- #1532: Converse reloads on enter pressed in the filter box
......@@ -34,14 +34,14 @@
- #1576: Converse gets stuck with spinner when logging out with `auto_login` set to `true`
- #1586: Not possible to kick someone with a space in their nickname
- **Breaking changes**:
### Breaking changes
- Rename `muc_disable_moderator_commands` to [muc_disable_slash_commands](https://conversejs.org/docs/html/configuration.html#muc-disable-slash-commands).
- `_converse.api.archive.query` now returns a Promise instead of accepting a callback functions.
- `_converse.api.disco.supports` now returns a Promise which resolves to a Boolean instead of an Array.
- The `forward_messages` config option (which was set to `false` by default) has been removed.
Use [message_carbons](https://conversejs.org/docs/html/configuration.html#message-carbons) instead.
### API changes
- `_converse.chats.open` and `_converse.rooms.open` now take a `force`
......@@ -51,6 +51,7 @@
- `_converse.api.emit` has been removed in favor of [\_converse.api.trigger](https://conversejs.org/docs/html/api/-_converse.api.html#.trigger)
- `_converse.updateSettings` has been removed in favor of [\_converse.api.settings.update](https://conversejs.org/docs/html/api/-_converse.api.settings.html#.update)
- `_converse.api.roster.get` now returns a promise.
- New API method [\_converse.api.disco.features.get](https://conversejs.org/docs/html/api/-_converse.api.disco.features.html#.get)
## 4.2.0 (2019-04-04)
......
......@@ -25,14 +25,15 @@
// 'prosody@conference.prosody.im',
// 'jdev@conference.jabber.org'
// ],
// websocket_url: 'ws://chat.example.org:5280/xmpp-websocket',
// bosh_service_url: 'http://chat.example.org:5280/http-bind/',
websocket_url: 'wss://conversejs.org/xmpp-websocket',
bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
view_mode: 'fullscreen',
notify_all_room_messages: [
'discuss@conference.conversejs.org'
],
enable_smacks: true,
muc_respect_autojoin: false,
// bosh_service_url: 'http://chat.example.org:5280/http-bind/',
bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
message_archiving: 'always',
debug: true
});
......
......@@ -635,6 +635,15 @@ The app servers are specified with the `push_app_servers`_ option.
Registering a push app server against a MUC domain is not (yet) standardized
and this feature should be considered experimental.
enable_smacks
-------------
* Default: ``false``
Determines whether `XEP-0198 Stream Management <https://xmpp.org/extensions/xep-0198.html>`_
support is turned on or not.
expose_rid_and_sid
------------------
......@@ -1376,6 +1385,16 @@ want to embed a chat into the page.
Alternatively you could use it with `view_mode`_ set to ``overlayed`` to create
a single helpdesk-type chat.
smacks_max_unacked_stanzas
--------------------------
* Default: ``5``
This setting relates to `XEP-0198 <https://xmpp.org/extensions/xep-0198.html>`_
and determines the number of stanzas to be sent before Converse will ask the
server for acknowledgement of those stanzas.
sounds_path
-----------
......
......@@ -71,8 +71,8 @@ and a list of servers that you can set up yourself on `xmpp.org <https://xmpp.or
.. _`BOSH-section`:
BOSH
====
BOSH (XMPP-over-HTTP)
=====================
Web-browsers do not allow the persistent, direct TCP socket connections used by
desktop XMPP clients to communicate with XMPP servers.
......@@ -113,26 +113,8 @@ use it in production.
Refer to the :ref:`bosh-service-url` configuration setting for information on
how to configure Converse to connect to a BOSH URL.
.. _`websocket-section`:
Websocket
=========
Websockets provide an alternative means of connection to an XMPP server from
your browser.
Websockets provide long-lived, bidirectional connections which do not rely on
HTTP. Therefore BOSH, which operates over HTTP, doesn't apply to websockets.
`Prosody <http://prosody.im>`_ (from version 0.10) and `Ejabberd <http://www.ejabberd.im>`_ support websocket connections, as
does the node-xmpp-bosh connection manager.
Refer to the :ref:`websocket-url` configuration setting for information on how to
configure Converse to connect to a websocket URL.
The Webserver
=============
Configuring your webserver for BOSH
-----------------------------------
Lets say the domain under which you host Converse is *example.org:80*,
but the domain of your connection manager or the domain of
......@@ -149,7 +131,7 @@ There are two ways in which you can solve this problem.
.. _CORS:
1. Cross-Origin Resource Sharing (CORS)
---------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CORS is a technique for overcoming browser restrictions related to the
`same-origin security policy <https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy>`_.
......@@ -158,8 +140,8 @@ CORS is enabled by adding an ``Access-Control-Allow-Origin`` header. Where this
is configured depends on what webserver is used for your file upload server.
2. Reverse-proxy
----------------
2. Reverse-proxy
~~~~~~~~~~~~~~~~
Another possible solution is to add a reverse proxy to a webserver such as Nginx or Apache to ensure that
all services you use are hosted under the same domain name and port.
......@@ -177,7 +159,7 @@ the cross-domain restriction is ``mysite.com/http-bind`` and not
Your ``nginx`` or ``apache`` configuration will look as follows:
Nginx
~~~~~
^^^^^
.. code-block:: nginx
......@@ -202,7 +184,7 @@ Nginx
}
Apache
~~~~~~
^^^^^^
.. code-block:: apache
......@@ -227,7 +209,7 @@ Apache
the above example).
This might be because your webserver and BOSH proxy have the same timeout
for BOSH requests. Because the webserver receives the request slightly earlier,
for BOSH requests. Because the webserver receives the request slightly earlier,
it gives up a few microseconds before the XMPP server’s empty result and thus returns a
504 error page containing HTML to browser, which then gets parsed as if its
XML.
......@@ -239,6 +221,70 @@ Apache
this problem.
.. _`websocket-section`:
Websocket
=========
Websockets provide an alternative means of connection to an XMPP server from
your browser.
Websockets provide long-lived, bidirectional connections which do not rely on
HTTP. Therefore BOSH, which operates over HTTP, doesn't apply to websockets.
`Prosody <http://prosody.im>`_ (from version 0.10) and `Ejabberd <http://www.ejabberd.im>`_ support websocket connections, as
does the node-xmpp-bosh connection manager.
Refer to the :ref:`websocket-url` configuration setting for information on how to
configure Converse to connect to a websocket URL.
Reverse-proxy for a websocket connection
----------------------------------------
Assuming your website is accessible on port ``443`` on the domain ``mysite.com``
and your XMPP server's websocket server is running at ``localhost:5280/xmpp-websocket``.
You can then set up your webserver as an SSL enabled reverse proxy in front of
your websocket endpoint.
The :ref:`websocket-url` value you'll want to pass in to ``converse.initialize`` is ``wss://mysite.com/xmpp-websocket``.
Your ``nginx`` will look as follows:
.. code-block:: nginx
http {
server {
listen 443
server_name mysite.com;
ssl on;
ssl_certificate /path/to/fullchain.pem; # Properly set the path here
ssl_certificate_key /path/to/privkey.pem; # Properly set the path here
location = / {
root /path/to/converse.js/; # Properly set the path here
index index.html;
}
location /xmpp-websocket {
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:5280;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
# CORS
location ~ .(ttf|ttc|otf|eot|woff|woff2|font.css|css|js)$ {
add_header Access-Control-Allow-Origin "*"; # Decide here whether you want to allow all or only a particular domain
root /path/to/converse.js/; # Properly set the path here
}
}
}
.. _`session-support`:
Single Session Support
......@@ -353,7 +399,7 @@ If your web-application has access to the same credentials, it can send those
credentials to Converse so that user's are automatically logged in when the
page loads.
This is can be done by setting :ref:`auto_login` to true and configuring the
This is can be done by setting :ref:`auto_login` to true and configuring the
the :ref:`credentials_url` setting.
Option 3). Temporary authentication tokens
......
......@@ -13702,8 +13702,8 @@
}
},
"strophe.js": {
"version": "github:strophe/strophejs#44da5faca8baa61c691739d63af8b1dea1d2436c",
"from": "github:strophe/strophejs#44da5faca8baa61c691739d63af8b1dea1d2436c"
"version": "github:strophe/strophejs#f52f26e8cc23f738b7b39180a7ee4511ccd41526",
"from": "github:strophe/strophejs#f52f26e8cc23f738b7b39180a7ee4511ccd41526"
},
"style-loader": {
"version": "0.23.1",
......
......@@ -32,7 +32,7 @@
delete _converse.jid;
_converse.keepalive = true;
_converse.authentication = "prebind";
expect(_converse.logIn.bind(_converse)).toThrow(
expect(_converse.api.user.login.bind(_converse)).toThrow(
new Error(
"restoreBOSHSession: tried to restore a \"keepalive\" session "+
"but we don't have the JID for the user!"));
......@@ -47,7 +47,7 @@
delete _converse.jid;
_converse.keepalive = false;
_converse.authentication = "prebind";
expect(_converse.logIn.bind(_converse)).toThrow(
expect(_converse.api.user.login.bind(_converse)).toThrow(
new Error("attemptPreboundSession: If you use prebind and not keepalive, then you MUST supply JID, RID and SID values or a prebind_url."));
_converse.bosh_service_url = undefined;
_converse.jid = jid;
......
......@@ -9,37 +9,33 @@
null, ['connectionInitialized', 'chatBoxesInitialized'],
{ auto_login: false,
allow_registration: false },
function (done, _converse) {
async function (done, _converse) {
test_utils.waitUntil(() => _converse.chatboxviews.get('controlbox'))
.then(function () {
var cbview = _converse.chatboxviews.get('controlbox');
test_utils.openControlBox();
const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]');
expect(checkboxes.length).toBe(1);
test_utils.openControlBox();
const cbview = await test_utils.waitUntil(() => _converse.chatboxviews.get('controlbox'));
const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]');
expect(checkboxes.length).toBe(1);
const checkbox = checkboxes[0];
const label = cbview.el.querySelector(`label[for="${checkbox.getAttribute('id')}"]`);
expect(label.textContent).toBe('This is a trusted device');
expect(checkbox.checked).toBe(true);
cbview.el.querySelector('input[name="jid"]').value = 'dummy@localhost';
cbview.el.querySelector('input[name="password"]').value = 'secret';
const checkbox = checkboxes[0];
const label = cbview.el.querySelector(`label[for="${checkbox.getAttribute('id')}"]`);
expect(label.textContent).toBe('This is a trusted device');
expect(checkbox.checked).toBe(true);
spyOn(cbview.loginpanel, 'connect');
cbview.delegateEvents();
cbview.el.querySelector('input[name="jid"]').value = 'dummy@localhost';
cbview.el.querySelector('input[name="password"]').value = 'secret';
expect(_converse.config.get('storage')).toBe('local');
cbview.el.querySelector('input[type="submit"]').click();
expect(_converse.config.get('storage')).toBe('local');
expect(cbview.loginpanel.connect).toHaveBeenCalled();
spyOn(cbview.loginpanel, 'connect');
cbview.delegateEvents();
expect(_converse.config.get('storage')).toBe('local');
cbview.el.querySelector('input[type="submit"]').click();
expect(_converse.config.get('storage')).toBe('local');
expect(cbview.loginpanel.connect).toHaveBeenCalled();
checkbox.click();
cbview.el.querySelector('input[type="submit"]').click();
expect(_converse.config.get('storage')).toBe('session');
done();
});
checkbox.click();
cbview.el.querySelector('input[type="submit"]').click();
expect(_converse.config.get('storage')).toBe('session');
done();
}));
it("checkbox can be set to false by default",
......
......@@ -273,7 +273,7 @@
'name': 'Nicky'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
// Check that the IQ set was acknowledged.
expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
expect(Strophe.serialize(sent_stanza)).toBe( // Strophe adds the xmlns attr (although not in spec)
`<iq from="dummy@localhost/resource" id="${IQ_id}" type="result" xmlns="jabber:client"/>`
);
expect(_converse.roster.updateContact).toHaveBeenCalled();
......
......@@ -5,6 +5,8 @@
const $iq = converse.env.$iq;
const Strophe = converse.env.Strophe;
const _ = converse.env._;
const sizzle = converse.env.sizzle;
const u = converse.env.utils;
describe("XEP-0357 Push Notifications", function () {
......@@ -56,31 +58,52 @@
}]
}, async function (done, _converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas,
room_jid = 'coven@chat.shakespeare.lit';
expect(_converse.session.get('push_enabled')).toBeFalsy();
test_utils.openAndEnterChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'oldhag');
const IQ_stanzas = _converse.connection.IQ_stanzas;
const room_jid = 'coven@chat.shakespeare.lit';
await test_utils.waitUntilDiscoConfirmed(
_converse, _converse.push_app_servers[0].jid,
[{'category': 'pubsub', 'type':'push'}],
['urn:xmpp:push:0'], [], 'info');
await test_utils.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid, [],
['urn:xmpp:push:0']);
let iq = await test_utils.waitUntil(() => _.filter(
IQ_stanzas,
iq => sizzle(`iq[type="set"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
).pop());
expect(Strophe.serialize(iq)).toBe(
`<iq id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<enable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0"/>`+
`</iq>`
);
const result = u.toStanza(`<iq type="result" id="${iq.getAttribute('id')}" to="dummy@localhost" />`);
_converse.connection._dataRecv(test_utils.createRequest(result));
await test_utils.waitUntil(() => _converse.session.get('push_enabled'));
expect(_converse.session.get('push_enabled').length).toBe(1);
expect(_.includes(_converse.session.get('push_enabled'), 'dummy@localhost')).toBe(true);
test_utils.openAndEnterChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'oldhag');
await test_utils.waitUntilDiscoConfirmed(
_converse, 'chat.shakespeare.lit',
[{'category': 'account', 'type':'registered'}],
['urn:xmpp:push:0'], [], 'info');
const stanza = await test_utils.waitUntil(
() => _.filter(IQ_stanzas, (iq) => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop()
);
expect(Strophe.serialize(stanza)).toEqual(
`<iq id="${stanza.getAttribute('id')}" to="chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
iq = await test_utils.waitUntil(() => _.filter(
IQ_stanzas,
iq => sizzle(`iq[type="set"][to="chat.shakespeare.lit"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
).pop());
expect(Strophe.serialize(iq)).toEqual(
`<iq id="${iq.getAttribute('id')}" to="chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
'<enable jid="push-5@client.example" node="yxs32uqsflafdk3iuqo" xmlns="urn:xmpp:push:0"/>'+
'</iq>'
);
_converse.connection._dataRecv(test_utils.createRequest($iq({
'to': _converse.connection.jid,
'type': 'result',
'id': stanza.getAttribute('id')
'id': iq.getAttribute('id')
})));
await test_utils.waitUntil(() => _.includes(_converse.session.get('push_enabled'), 'chat.shakespeare.lit'));
done();
......
(function (root, factory) {
define(["jasmine", "mock", "test-utils"], factory);
} (this, function (jasmine, mock, test_utils) {
"use strict";
const $iq = converse.env.$iq;
const Strophe = converse.env.Strophe;
const sizzle = converse.env.sizzle;
const u = converse.env.utils;
describe("XEP-0198 Stream Management", function () {
it("gets enabled with an <enable> stanza and resumed with a <resume> stanza",
mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'],
{ 'auto_login': false,
'enable_smacks': true,
'show_controlbox_by_default': true,
'smacks_max_unacked_stanzas': 2
},
async function (done, _converse) {
const view = _converse.chatboxviews.get('controlbox');
spyOn(view, 'renderControlBoxPane').and.callThrough();
_converse.api.user.login('dummy@localhost', 'secret');
const sent_stanzas = _converse.connection.sent_stanzas;
let stanza = await test_utils.waitUntil(() =>
sent_stanzas.filter(s => (s.tagName === 'enable')).pop());
expect(_converse.session.get('smacks_enabled')).toBe(false);
expect(Strophe.serialize(stanza)).toEqual('<enable resume="true" xmlns="urn:xmpp:sm:3"/>');
let result = u.toStanza(`<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true"/>`);
_converse.connection._dataRecv(test_utils.createRequest(result));
expect(_converse.session.get('smacks_enabled')).toBe(true);
await test_utils.waitUntil(() => view.renderControlBoxPane.calls.count());
let IQ_stanzas = _converse.connection.IQ_stanzas;
await test_utils.waitUntil(() => IQ_stanzas.length === 4);
let iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="dummy@localhost/resource" id="${iq.getAttribute('id')}" to="dummy@localhost" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq id="${iq.getAttribute('id')}" type="get" xmlns="jabber:client"><query xmlns="jabber:iq:roster"/></iq>`);
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="dummy@localhost/resource" id="${iq.getAttribute('id')}" to="localhost" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
const disco_iq = IQ_stanzas.pop();
expect(Strophe.serialize(disco_iq)).toBe(
`<iq from="dummy@localhost" id="${disco_iq.getAttribute('id')}" to="dummy@localhost" type="get" xmlns="jabber:client">`+
`<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(2);
expect(_converse.session.get('unacked_stanzas').length).toBe(4);
// test handling of acks
let ack = u.toStanza(`<a xmlns="urn:xmpp:sm:3" h="1"/>`);
_converse.connection._dataRecv(test_utils.createRequest(ack));
expect(_converse.session.get('unacked_stanzas').length).toBe(3);
// test handling of ack requests
let r = u.toStanza(`<r xmlns="urn:xmpp:sm:3"/>`);
_converse.connection._dataRecv(test_utils.createRequest(r));
ack = await test_utils.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a')).pop());
expect(Strophe.serialize(ack)).toBe('<a h="0" xmlns="urn:xmpp:sm:3"/>');
const disco_result = $iq({
'type': 'result',
'from': 'localhost',
'to': 'dummy@localhost/resource',
'id': disco_iq.getAttribute('id'),
}).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
.c('identity', {
'category': 'server',
'type': 'im'
}).up()
.c('feature', {'var': 'http://jabber.org/protocol/disco#info'}).up()
.c('feature', {'var': 'http://jabber.org/protocol/disco#items'});
_converse.connection._dataRecv(test_utils.createRequest(disco_result));
ack = u.toStanza(`<a xmlns="urn:xmpp:sm:3" h="2"/>`);
_converse.connection._dataRecv(test_utils.createRequest(ack));
expect(_converse.session.get('unacked_stanzas').length).toBe(2);
r = u.toStanza(`<r xmlns="urn:xmpp:sm:3"/>`);
_converse.connection._dataRecv(test_utils.createRequest(r));
ack = await test_utils.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a' && s.getAttribute('h') === '1')).pop());
expect(Strophe.serialize(ack)).toBe('<a h="1" xmlns="urn:xmpp:sm:3"/>');
// test session resumption
_converse.connection.IQ_stanzas = [];
IQ_stanzas = _converse.connection.IQ_stanzas;
_converse.api.connection.reconnect();
stanza = await test_utils.waitUntil(() =>
sent_stanzas.filter(s => (s.tagName === 'resume')).pop());
expect(Strophe.serialize(stanza)).toEqual('<resume h="2" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>');
result = u.toStanza(`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`);
_converse.connection._dataRecv(test_utils.createRequest(result));
// Another <enable> stanza doesn't get sent out
expect(sizzle('enable', sent_stanzas).length).toBe(0);
expect(_converse.session.get('smacks_enabled')).toBe(true);
await test_utils.waitUntil(() => IQ_stanzas.length === 2);
// Test that unacked stanzas get resent out
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="dummy@localhost/resource" id="${iq.getAttribute('id')}" to="dummy@localhost" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq id="${iq.getAttribute('id')}" type="get" xmlns="jabber:client"><query xmlns="jabber:iq:roster"/></iq>`);
done();
}));
});
}));
......@@ -117,7 +117,7 @@
});
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
/* Test the XML stanza
/* Test the XML stanza
*
* <message from="dummy@localhost/resource"
* to="max.frankfurter@localhost"
......@@ -194,7 +194,7 @@
});
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
/* Test the XML stanza
/* Test the XML stanza
*
* <message from="dummy@localhost/resource"
* to="max.frankfurter@localhost"
......
......@@ -438,8 +438,7 @@ converse.plugins.add('converse-controlbox', {
*/
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (_converse.authentication === _converse.ANONYMOUS) {
this.connect(_converse.jid, null);
return;
return this.connect(_converse.jid, null);
}
if (!this.validate()) { return; }
......@@ -467,24 +466,16 @@ converse.plugins.add('converse-controlbox', {
} else if (_converse.default_domain && !_.includes(jid, '@')) {
jid = jid + '@' + _converse.default_domain;
}
this.connect(jid, form_data.get('password'));
this.connect(jid, form_data.get('password'));
},
connect (jid, password) {
if (jid) {
const resource = Strophe.getResourceFromJid(jid);
if (!resource) {
jid = jid.toLowerCase() + _converse.generateResource();
} else {
jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+resource;
}
}
if (_.includes(["converse/login", "converse/register"],
Backbone.history.getFragment())) {
_converse.router.navigate('', {'replace': true});
}
_converse.connection.reset();
_converse.connection.connect(jid, password, _converse.onConnectStatusChanged);
_converse.api.user.login(jid, password);
}
});
......
......@@ -285,7 +285,7 @@ converse.plugins.add('converse-profile', {
ev.preventDefault();
const result = confirm(__("Are you sure you want to log out?"));
if (result === true) {
_converse.logOut();
_converse.api.user.logout();
}
},
......
......@@ -106,9 +106,9 @@ converse.plugins.add('converse-push', {
}
const enabled_services = _.reject(_converse.push_app_servers, 'disable');
const disabled_services = _.filter(_converse.push_app_servers, 'disable');
const enabled = _.map(enabled_services, _.partial(enablePushAppServer, domain));
const disabled = _.map(disabled_services, _.partial(disablePushAppServer, domain));
try {
const enabled = _.map(enabled_services, _.partial(enablePushAppServer, domain));
const disabled = _.map(disabled_services, _.partial(disablePushAppServer, domain));
await Promise.all(enabled.concat(disabled));
} catch (e) {
_converse.log('Could not enable or disable push App Server', Strophe.LogLevel.ERROR);
......@@ -118,7 +118,6 @@ converse.plugins.add('converse-push', {
}
_converse.session.save('push_enabled', push_enabled);
}
_converse.api.listen.on('statusInitialized', () => enablePush());
function onChatBoxAdded (model) {
......
......@@ -102,6 +102,7 @@ _converse.core_plugins = [
'converse-pubsub',
'converse-roster',
'converse-rsm',
'converse-smacks',
'converse-vcard'
];
......@@ -190,7 +191,7 @@ _converse.CHATROOMS_TYPE = 'chatroom';
_converse.HEADLINES_TYPE = 'headline';
_converse.CONTROLBOX_TYPE = 'controlbox';
_converse.default_connection_options = {};
_converse.default_connection_options = {'explicitResourceBinding': true};
// Default configuration values
// ----------------------------
......@@ -304,8 +305,9 @@ _converse.__ = function (str) {
const __ = _converse.__;
const PROMISES = [
'initialized',
'afterResourceBinding',
'connectionInitialized',
'initialized',
'pluginsInitialized',
'statusInitialized'
];
......@@ -405,6 +407,34 @@ function initClientConfig () {
_converse.api.trigger('clientConfigInitialized');
}
function clearSession () {
if (!_.isUndefined(_converse.bosh_session)) {
_converse.bosh_session.destroy();
delete _converse.bosh_session;
}
if (!_.isUndefined(_converse.session)) {
_converse.session.destroy();
delete _converse.session;
}
// TODO: Refactor so that we don't clear
if (!_converse.config.get('trusted') || isTestEnv()) {
window.localStorage.clear();
window.sessionStorage.clear();
} else {
_.get(_converse, 'bosh_session.browserStorage', {'_clear': _.noop})._clear();
_.get(_converse, 'session.browserStorage', {'_clear': _.noop})._clear();
}
/**
* Triggered once the session information has been cleared,
* for example when the user has logged out or when Converse has
* disconnected for some other reason.
* @event _converse#clearSession
*/
_converse.api.trigger('clearSession');
}
_converse.initConnection = function () {
/* Creates a new Strophe.Connection instance if we don't already have one.
*/
......@@ -437,28 +467,76 @@ _converse.initConnection = function () {
}
async function initSession () {
async function initBOSHSession () {
const id = 'converse.bosh-session';
_converse.session = new Backbone.Model({id});
_converse.session.browserStorage = new BrowserStorage.session(id);
_converse.bosh_session = new Backbone.Model({id});
_converse.bosh_session.browserStorage = new BrowserStorage.session(id);
try {
await new Promise((success, error) => _converse.session.fetch({success, error}));
if (_converse.jid && !u.isSameBareJID(_converse.session.get('jid'), _converse.jid)) {
_converse.session.clear({'silent': true});
_converse.session.save({'jid': _converse.jid, id});
await new Promise((success, error) => _converse.bosh_session.fetch({success, error}));
if (_converse.jid && !u.isSameBareJID(_converse.bosh_session.get('jid'), _converse.jid)) {
_converse.bosh_session.clear({'silent': true});
_converse.bosh_session.save({'jid': _converse.jid, id});
}
} catch (e) {
if (_converse.jid) {
_converse.session.save({'jid': _converse.jid});
_converse.bosh_session.save({'jid': _converse.jid});
}
}
/**
* Triggered once the session has been initialized. The session is a
* persistent object which stores session information in the browser storage.
* @event _converse#sessionInitialized
* @event _converse#BOSHSessionInitialized
* @memberOf _converse
*/
_converse.api.trigger('sessionInitialized');
_converse.api.trigger('BOSHSessionInitialized');
}
async function initUserSession (jid) {
const bare_jid = Strophe.getBareJidFromJid(jid);
const id = `converse.session-${bare_jid}`;
if (!_converse.session || _converse.session.get('id') !== id) {
_converse.session = new Backbone.Model({id});
_converse.session.browserStorage = new BrowserStorage.session(id);
await new Promise(r => _converse.session.fetch({'success': r, 'error': r}));
/**
* Triggered once the user's session has been initialized. The session is a
* cache which stores information about the user's current session.
* @event _converse#userSessionInitialized
* @memberOf _converse
*/
_converse.api.trigger('userSessionInitialized');
}
}
function setUserJID (jid) {
initUserSession(jid);
_converse.jid = jid;
_converse.bare_jid = Strophe.getBareJidFromJid(jid);
_converse.resource = Strophe.getResourceFromJid(jid);
_converse.domain = Strophe.getDomainFromJid(jid);
_converse.session.save({
'jid': jid,
'bare_jid': _converse.bare_jid,
'resource': _converse.resource,
'domain': _converse.domain
});
}
async function onConnected (reconnecting) {
/* Called as soon as a new connection has been established, either
* by logging in or by attaching to an existing BOSH session.
*/
_converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser
setUserJID(_converse.connection.jid);
/**
* Synchronous event triggered after we've sent an IQ to bind the
* user's JID resource for this session.
* @event _converse#afterResourceBinding
*/
await _converse.api.trigger('afterResourceBinding', {'synchronous': true});
_converse.enableCarbons();
_converse.initStatus(reconnecting)
}
......@@ -483,8 +561,8 @@ async function finishInitialization () {
initClientConfig();
initPlugins();
_converse.initConnection();
await initSession();
_converse.logIn();
await initBOSHSession();
_converse.api.user.login();
_converse.registerGlobalEventHandlers();
if (!Backbone.history.started) {
Backbone.history.start();
......@@ -758,7 +836,7 @@ _converse.initialize = async function (settings, callback) {
_converse.connection.reconnecting = true;
_converse.tearDown();
_converse.logIn(null, true);
_converse.api.user.login(null, null, true);
}, 2000);
......@@ -773,7 +851,7 @@ _converse.initialize = async function (settings, callback) {
delete _converse.connection.reconnecting;
_converse.connection.reset();
_converse.tearDown();
_converse.clearSession();
clearSession();
/**
* Triggered after converse.js has disconnected from the XMPP server.
* @event _converse#disconnected
......@@ -840,7 +918,7 @@ _converse.initialize = async function (settings, callback) {
_converse.setDisconnectionCause();
if (_converse.connection.reconnecting) {
_converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
_converse.onConnected(true);
onConnected(true);
} else {
_converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached');
if (_converse.connection.restored) {
......@@ -848,7 +926,7 @@ _converse.initialize = async function (settings, callback) {
// we're restoring an existing session.
_converse.send_initial_presence = false;
}
_converse.onConnected();
onConnected();
}
} else if (status === Strophe.Status.DISCONNECTED) {
_converse.setDisconnectionCause(status, message);
......@@ -928,39 +1006,6 @@ _converse.initialize = async function (settings, callback) {
}
}
this.clearSession = function () {
if (!_converse.config.get('trusted') || isTestEnv()) {
window.localStorage.clear();
window.sessionStorage.clear();
} else {
_.get(_converse, 'session.browserStorage', {'_clear': _.noop})._clear();
}
/**
* Triggered once the session information has been cleared,
* for example when the user has logged out or when Converse has
* disconnected for some other reason.
* @event _converse#clearSession
*/
_converse.api.trigger('clearSession');
};
this.logOut = function () {
_converse.clearSession();
_converse.setDisconnectionCause(_converse.LOGOUT, undefined, true);
if (!_.isUndefined(_converse.connection)) {
_converse.connection.disconnect();
} else {
_converse.tearDown();
}
// Recreate all the promises
Object.keys(_converse.promises).forEach(addPromise);
/**
* Triggered once the user has logged out.
* @event _converse#logout
*/
_converse.api.trigger('logout');
};
this.saveWindowState = function (ev) {
// XXX: eventually we should be able to just use
// document.visibilityState (when we drop support for older
......@@ -1013,7 +1058,7 @@ _converse.initialize = async function (settings, callback) {
/* Ask the XMPP server to enable Message Carbons
* See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
*/
if (!this.message_carbons || this.session.get('carbons_enabled')) {
if (!this.message_carbons || !this.session || !this.session.get('carbons_enabled')) {
return;
}
const carbons_iq = new Strophe.Builder('iq', {
......@@ -1076,25 +1121,6 @@ _converse.initialize = async function (settings, callback) {
}
};
this.setUserJID = function () {
_converse.jid = _converse.connection.jid;
_converse.bare_jid = Strophe.getBareJidFromJid(_converse.connection.jid);
_converse.resource = Strophe.getResourceFromJid(_converse.connection.jid);
_converse.domain = Strophe.getDomainFromJid(_converse.connection.jid);
_converse.session.save({
'jid': _converse.connection.jid,
'bare_jid': Strophe.getBareJidFromJid(_converse.connection.jid),
'resource': Strophe.getResourceFromJid(_converse.connection.jid),
'domain': Strophe.getDomainFromJid(_converse.connection.jid)
});
/**
* Triggered once we have the user's full JID and it's been save in the
* session.
* @event _converse#setUserJID
*/
_converse.api.trigger('setUserJID');
};
this.bindResource = async function () {
/**
* Synchronous event triggered before we send an IQ to bind the user's
......@@ -1105,17 +1131,6 @@ _converse.initialize = async function (settings, callback) {
_converse.connection.bind();
};
this.onConnected = function (reconnecting) {
/* Called as soon as a new connection has been established, either
* by logging in or by attaching to an existing BOSH session.
*/
_converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser
_converse.setUserJID();
_converse.enableCarbons();
_converse.initStatus(reconnecting)
};
this.ConnectionFeedback = Backbone.Model.extend({
defaults: {
'connection_status': Strophe.Status.DISCONNECTED,
......@@ -1130,12 +1145,8 @@ _converse.initialize = async function (settings, callback) {
this.XMPPStatus = Backbone.Model.extend({
defaults () {
return {
"jid": _converse.bare_jid,
"status": _converse.default_state
}
defaults: {
"status": _converse.default_state
},
initialize () {
......@@ -1237,7 +1248,7 @@ _converse.initialize = async function (settings, callback) {
return false;
}
/* Tries to restore a cached BOSH session. */
const jid = _converse.session.get('jid');
const jid = _converse.bosh_session.get('jid');
if (!jid) {
const msg = "restoreBOSHSession: tried to restore a \"keepalive\" session "+
"but we don't have the JID for the user!";
......@@ -1256,7 +1267,7 @@ _converse.initialize = async function (settings, callback) {
_converse.log(
"Could not restore session for jid: "+
jid+" Error message: "+e.message, Strophe.LogLevel.WARN);
this.clearSession(); // We want to clear presences (see #555)
clearSession(); // We want to clear presences (see #555)
return false;
}
}
......@@ -1323,11 +1334,6 @@ _converse.initialize = async function (settings, callback) {
};
this.autoLogin = function (credentials) {
if (credentials) {
// If passed in, the credentials come from credentials_url,
// so we set them on the converse object.
this.jid = credentials.jid;
}
if (this.authentication === _converse.ANONYMOUS || this.authentication === _converse.EXTERNAL) {
if (!this.jid) {
throw new Error("Config Error: when using anonymous login " +
......@@ -1350,12 +1356,6 @@ _converse.initialize = async function (settings, callback) {
_converse.api.connection.disconnect();
return;
}
const resource = Strophe.getResourceFromJid(this.jid);
if (!resource) {
this.jid = this.jid.toLowerCase() + _converse.generateResource();
} else {
this.jid = Strophe.getBareJidFromJid(this.jid).toLowerCase()+'/'+resource;
}
if (!this.connection.reconnecting) {
this.connection.reset();
}
......@@ -1363,20 +1363,10 @@ _converse.initialize = async function (settings, callback) {
}
};
this.logIn = function (credentials, reconnecting) {
// We now try to resume or automatically set up a new session.
// Otherwise the user will be shown a login form.
if (this.authentication === _converse.PREBIND) {
this.attemptPreboundSession(reconnecting);
} else {
this.attemptNonPreboundSession(credentials, reconnecting);
}
};
this.tearDown = function () {
_converse.api.trigger('beforeTearDown');
if (!_.isUndefined(_converse.session)) {
_converse.session.destroy();
if (!_.isUndefined(_converse.bosh_session)) {
_converse.bosh_session.destroy();
}
window.removeEventListener('click', _converse.onUserActivity);
window.removeEventListener('focus', _converse.onUserActivity);
......@@ -1451,7 +1441,7 @@ _converse.api = {
_converse.connection.disconnect();
} else {
_converse.tearDown();
_converse.clearSession();
clearSession();
}
},
},
......@@ -1473,7 +1463,7 @@ _converse.api = {
/* Event emitter and promise resolver */
const args = Array.from(arguments);
const options = args.pop();
if (options.synchronous) {
if (options && options.synchronous) {
const events = _converse._events[name] || [];
await Promise.all(events.map(e => e.callback.call(e.ctx, args)));
} else {
......@@ -1507,31 +1497,59 @@ _converse.api = {
* to log the user in by calling the `prebind_url` or `credentials_url` depending
* on whether prebinding is used or not.
*
* Otherwise the user will be shown a login form.
*
* @method _converse.api.user.login
* @param {object} [credentials] An object with the credentials.
* @param {string} [jid]
* @param {string} [password]
* @param {boolean} [reconnecting]
* @example
* converse.plugins.add('myplugin', {
* initialize: function () {
*
* this._converse.api.user.login({
* 'jid': 'dummy@example.com',
* 'password': 'secret'
* });
*
* this._converse.api.user.login('dummy@example.com', 'secret');
* }
* });
*/
'login' (credentials) {
_converse.logIn(credentials);
login (jid, password, reconnecting) {
if (_converse.authentication === _converse.PREBIND) {
_converse.attemptPreboundSession(reconnecting);
} else {
let credentials;
if (jid) {
const resource = Strophe.getResourceFromJid(jid);
if (!resource) {
jid = jid.toLowerCase() + _converse.generateResource();
} else {
jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+resource;
}
setUserJID(jid);
credentials = {'jid': jid, 'password': password};
}
_converse.attemptNonPreboundSession(credentials, reconnecting);
}
},
/**
* Logs the user out of the current XMPP session.
*
* @method _converse.api.user.logout
* @example _converse.api.user.logout();
*/
'logout' () {
_converse.logOut();
logout () {
clearSession();
_converse.setDisconnectionCause(_converse.LOGOUT, undefined, true);
if (!_.isUndefined(_converse.connection)) {
_converse.connection.disconnect();
} else {
_converse.tearDown();
}
// Recreate all the promises
Object.keys(_converse.promises).forEach(addPromise);
/**
* Triggered once the user has logged out.
* @event _converse#logout
*/
_converse.api.trigger('logout');
},
/**
* Set and get the user's chat status, also called their *availability*.
......@@ -1841,9 +1859,16 @@ _converse.api = {
* });
* _converse.api.send(msg);
*/
'send' (stanza) {
_converse.connection.send(stanza);
_converse.api.trigger('send', stanza);
send (stanza) {
if (_.isString(stanza)) {
stanza = u.toStanza(stanza);
}
if (stanza.tagName === 'iq') {
return _converse.api.sendIQ(stanza);
} else {
_converse.connection.send(stanza);
_converse.api.trigger('send', stanza);
}
},
/**
......@@ -1852,7 +1877,7 @@ _converse.api = {
* @returns {Promise} A promise which resolves when we receive a `result` stanza
* or is rejected when we receive an `error` stanza.
*/
'sendIQ' (stanza, timeout) {
sendIQ (stanza, timeout) {
return new Promise((resolve, reject) => {
_converse.connection.sendIQ(stanza, resolve, reject, timeout || _converse.IQ_TIMEOUT);
_converse.api.trigger('send', stanza);
......
......@@ -22,6 +22,7 @@ converse.plugins.add('converse-disco', {
// Promises exposed by this plugin
_converse.api.promises.add('discoInitialized');
_converse.api.promises.add('streamFeaturesAdded');
/**
......@@ -260,32 +261,33 @@ converse.plugins.add('converse-disco', {
}
function initStreamFeatures () {
_converse.stream_features = new Backbone.Collection();
_converse.stream_features.browserStorage = new BrowserStorage.session(
`converse.stream-features-${_converse.bare_jid}`
);
_converse.stream_features.fetch({
success (collection) {
if (collection.length === 0 && _converse.connection.features) {
_.forEach(
_converse.connection.features.childNodes,
(feature) => {
_converse.stream_features.create({
'name': feature.nodeName,
'xmlns': feature.getAttribute('xmlns')
const bare_jid = Strophe.getBareJidFromJid(_converse.jid);
const id = `converse.stream-features-${bare_jid}`;
if (!_converse.stream_features || _converse.stream_features.browserStorage.id !== id) {
_converse.stream_features = new Backbone.Collection();
_converse.stream_features.browserStorage = new BrowserStorage.session(id);
_converse.stream_features.fetch({
success (collection) {
if (collection.length === 0 && _converse.connection.features) {
Array.from(_converse.connection.features.childNodes)
.forEach(feature => {
_converse.stream_features.create({
'name': feature.nodeName,
'xmlns': feature.getAttribute('xmlns')
});
});
});
}
/**
* Triggered as soon as Converse has processed the stream features as advertised by
* the server. If you want to check whether a stream feature is supported before
* proceeding, then you'll first want to wait for this event.
* @event _converse#streamFeaturesAdded
* @example _converse.api.listen.on('streamFeaturesAdded', () => { ... });
*/
_converse.api.trigger('streamFeaturesAdded');
}
}
});
/**
* Triggered as soon as Converse has processed the stream features as advertised by
* the server. If you want to check whether a stream feature is supported before
* proceeding, then you'll first want to wait for this event.
* @event _converse#streamFeaturesAdded
* @example _converse.api.listen.on('streamFeaturesAdded', () => { ... });
*/
_converse.api.trigger('streamFeaturesAdded');
});
}
}
async function initializeDisco () {
......@@ -313,7 +315,9 @@ converse.plugins.add('converse-disco', {
_converse.api.trigger('discoInitialized');
}
_converse.api.listen.on('setUserJID', initStreamFeatures);
_converse.api.listen.on('userSessionInitialized', initStreamFeatures);
_converse.api.listen.on('beforeResourceBinding', initStreamFeatures);
_converse.api.listen.on('reconnected', initializeDisco);
_converse.api.listen.on('connected', initializeDisco);
......@@ -326,6 +330,10 @@ converse.plugins.add('converse-disco', {
_converse.disco_entities.reset();
_converse.disco_entities.browserStorage._clear();
}
if (_converse.stream_features) {
_converse.stream_features.reset();
_converse.stream_features.browserStorage._clear();
}
});
const plugin = this;
......@@ -386,7 +394,8 @@ converse.plugins.add('converse-disco', {
* @param {String} xmlns The XML namespace
* @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver')
*/
'getFeature': function (name, xmlns) {
'getFeature': async function (name, xmlns) {
await _converse.api.waitUntil('streamFeaturesAdded');
if (_.isNil(name) || _.isNil(xmlns)) {
throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature");
}
......
// Converse.js
// http://conversejs.org
//
// Copyright (c) The Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
/* This is a Converse.js plugin which add support for XEP-0198: Stream Management */
import converse from "./converse-core";
const { Strophe, $build, _ } = converse.env;
const u = converse.env.utils;
Strophe.addNamespace('SM', 'urn:xmpp:sm:3');
converse.plugins.add('converse-smacks', {
initialize () {
const { _converse } = this;
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
_converse.api.settings.update({
'enable_smacks': false,
'smacks_max_unacked_stanzas': 5,
});
function isStreamManagementSupported () {
return _converse.api.disco.stream.getFeature('sm', Strophe.NS.SM);
}
function handleAck (el) {
if (!_converse.session.get('smacks_enabled')) {
return true;
}
const handled = parseInt(el.getAttribute('h'), 10);
const last_known_handled = _converse.session.get('num_stanzas_handled_by_server');
const delta = handled - last_known_handled;
if (delta < 0) {
const err_msg = `New reported stanza count lower than previous. `+
`New: ${handled} - Previous: ${last_known_handled}`
_converse.log(err_msg, Strophe.LogLevel.ERROR);
}
const unacked_stanzas = _converse.session.get('unacked_stanzas');
if (delta > unacked_stanzas.length) {
const err_msg =
`Higher reported acknowledge count than unacknowledged stanzas. `+
`Reported Acknowledged Count: ${delta} -`+
`Unacknowledged Stanza Count: ${unacked_stanzas.length} -`+
`New: ${handled} - Previous: ${last_known_handled}`
_converse.log(err_msg, Strophe.LogLevel.ERROR);
}
_converse.session.save({
'num_stanzas_handled_by_server': handled,
'num_stanzas_since_last_ack': 0,
'unacked_stanzas': unacked_stanzas.slice(delta)
});
return true;
}
function sendAck() {
if (_converse.session.get('smacks_enabled')) {
const h = _converse.session.get('num_stanzas_handled');
const stanza = u.toStanza(`<a xmlns="${Strophe.NS.SM}" h="${h}"/>`);
_converse.api.send(stanza);
}
return true;
}
function stanzaHandler (el) {
if (_converse.session.get('smacks_enabled')) {
if (u.isTagEqual(el, 'iq') || u.isTagEqual(el, 'presence') || u.isTagEqual(el, 'message')) {
const h = _converse.session.get('num_stanzas_handled');
_converse.session.save('num_stanzas_handled', h+1);
}
}
return true;
}
function clearSessionData () {
_converse.session.save({
'smacks_enabled': false,
'num_stanzas_handled': 0,
'num_stanzas_handled_by_server': 0,
'num_stanzas_since_last_ack': 0,
'unacked_stanzas': []
});
}
function saveSessionData (el) {
const data = {'smacks_enabled': true};
if (['1', 'true'].includes(el.getAttribute('resume'))) {
data['smacks_stream_id'] = el.getAttribute('id');
}
_converse.session.save(data);
return true;
}
function onFailedStanza (el) {
if (el.querySelector('item-not-found')) {
// Stream resumption must happen before resource binding but
// enabling a new stream must happen after resource binding.
// Since resumption failed, we simply continue.
//
// After resource binding, sendEnableStanza will be called
// based on the afterResourceBinding event.
_converse.log('Could not resume previous SMACKS session, session id not found. '+
'A new session will be established.', Strophe.LogLevel.WARN);
} else {
_converse.log('Failed to enable stream management', Strophe.LogLevel.ERROR);
_converse.log(el.outerHTML, Strophe.LogLevel.ERROR);
}
clearSessionData();
return true;
}
function resendUnackedStanzas () {
const stanzas = _converse.session.get('unacked_stanzas');
// We clear the unacked_stanzas array because it'll get populated
// again in `onStanzaSent`
_converse.session.save('unacked_stanzas', []);
// XXX: Currently we're resending *all* unacked stanzas, including
// IQ[type="get"] stanzas that longer have handlers (because the
// page reloaded or we reconnected, causing removal of handlers).
//
// *Side-note:* Is it necessary to clear handlers upon reconnection?
//
// I've considered not resending those stanzas, but then keeping
// track of what's been sent and ack'd and their order gets
// prohibitively complex.
//
// It's unclear how much of a problem this poses.
//
// Two possible solutions are running @converse/headless as a
// service worker or handling IQ[type="result"] stanzas
// differently, more like push stanzas, so that they don't need
// explicit handlers.
stanzas.forEach(s => _converse.api.send(s));
}
function onResumedStanza (el, resolve) {
saveSessionData(el);
handleAck(el);
resendUnackedStanzas();
_converse.connection.do_bind = false; // No need to bind our resource anymore
_converse.connection.authenticated = true;
_converse.connection._changeConnectStatus(Strophe.Status.CONNECTED, null);
}
async function sendResumeStanza () {
const promise = u.getResolveablePromise();
_converse.connection._addSysHandler(_.flow(onResumedStanza, promise.resolve), Strophe.NS.SM, 'resumed');
_converse.connection._addSysHandler(_.flow(onFailedStanza, promise.resolve), Strophe.NS.SM, 'failed');
const previous_id = _converse.session.get('smacks_stream_id');
const h = _converse.session.get('num_stanzas_handled_by_server');
const stanza = u.toStanza(`<resume xmlns="${Strophe.NS.SM}" h="${h}" previd="${previous_id}"/>`);
_converse.api.send(stanza);
_converse.connection.flush();
await promise;
}
async function sendEnableStanza () {
if (!_converse.enable_smacks || _converse.session.get('smacks_enabled')) {
return;
}
if (await isStreamManagementSupported()) {
const promise = u.getResolveablePromise();
_converse.connection._addSysHandler(_.flow(saveSessionData, promise.resolve), Strophe.NS.SM, 'enabled');
_converse.connection._addSysHandler(_.flow(onFailedStanza, promise.resolve), Strophe.NS.SM, 'failed');
const stanza = u.toStanza(`<enable xmlns="${Strophe.NS.SM}" resume="true"/>`);
_converse.api.send(stanza);
_converse.connection.flush();
await promise;
}
}
async function enableStreamManagement () {
if (!_converse.enable_smacks) {
return;
}
if (!(await isStreamManagementSupported())) {
return;
}
_converse.connection.addHandler(stanzaHandler);
_converse.connection.addHandler(sendAck, Strophe.NS.SM, 'r');
_converse.connection.addHandler(handleAck, Strophe.NS.SM, 'a');
if (_converse.connection._proto instanceof Strophe.Bosh &&
_converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED) {
// No need to continue further when we have an existing BOSH session,
// since our existing session still exists server-side.
return;
}
if (_converse.session.get('smacks_stream_id')) {
await sendResumeStanza();
} else {
clearSessionData();
}
}
function onStanzaSent (stanza) {
if (!_converse.session) {
_converse.log('No _converse.session!', Strophe.LogLevel.WARN);
return;
}
if (!_converse.session.get('smacks_enabled')) {
return;
}
if (u.isTagEqual(stanza, 'iq') ||
u.isTagEqual(stanza, 'presence') ||
u.isTagEqual(stanza, 'message')) {
const stanza_string = Strophe.serialize(stanza);
_converse.session.save(
'unacked_stanzas',
_converse.session.get('unacked_stanzas').concat([stanza_string])
);
const max_unacked = _converse.smacks_max_unacked_stanzas;
if (max_unacked > 0) {
const num = _converse.session.get('num_stanzas_since_last_ack') + 1;
if (num % max_unacked === 0) {
// Request confirmation of sent stanzas
_converse.api.send(u.toStanza(`<r xmlns="${Strophe.NS.SM}"/>`));
}
_converse.session.save({'num_stanzas_since_last_ack': num});
}
}
}
_converse.api.listen.on('beforeResourceBinding', enableStreamManagement);
_converse.api.listen.on('afterResourceBinding', sendEnableStanza);
_converse.api.listen.on('send', onStanzaSent);
}
});
......@@ -55,7 +55,7 @@ converse.plugins.add('converse-vcard', {
model: _converse.VCard,
initialize () {
this.on('add', (vcard) => _converse.api.vcard.update(vcard));
this.on('add', vcard => _converse.api.vcard.update(vcard));
}
});
......@@ -125,19 +125,17 @@ converse.plugins.add('converse-vcard', {
_converse.vcards.browserStorage = new BrowserStorage[_converse.config.get('storage')](id);
_converse.vcards.fetch();
}
_converse.api.listen.on('setUserJID', _converse.initVCardCollection);
_converse.api.listen.on('afterResourceBinding', _converse.initVCardCollection);
_converse.api.listen.on('statusInitialized', () => {
const vcards = _converse.vcards;
const jid = _converse.xmppstatus.get('jid');
const jid = _converse.session.get('bare_jid');
_converse.xmppstatus.vcard = vcards.findWhere({'jid': jid}) || vcards.create({'jid': jid});
});
_converse.api.listen.on('addClientFeatures', () => {
_converse.api.disco.own.features.add(Strophe.NS.VCARD);
});
_converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.VCARD));
/************************ BEGIN API ************************/
Object.assign(_converse.api, {
......@@ -191,7 +189,7 @@ converse.plugins.add('converse-vcard', {
* );
* });
*/
'get' (model, force) {
get (model, force) {
if (_.isString(model)) {
return getVCard(_converse, model);
} else if (force ||
......@@ -224,7 +222,7 @@ converse.plugins.add('converse-vcard', {
* _converse.api.vcard.update(chatbox);
* });
*/
'update' (model, force) {
update (model, force) {
return this.get(model, force)
.then(vcard => {
delete vcard['stanza']
......
......@@ -12,6 +12,7 @@ import "./converse-ping"; // XEP-0199 XMPP Ping
import "./converse-pubsub"; // XEP-0199 XMPP Ping
import "./converse-roster"; // Contacts Roster
import "./converse-rsm"; // XEP-0059 Result Set management
import "./converse-smacks"; // XEP-0198 Stream Management
import "./converse-vcard"; // XEP-0054 VCard-temp
/* END: Removable components */
......
......@@ -29,7 +29,7 @@
"jed": "1.1.1",
"lodash": "^4.17.11",
"pluggable.js": "2.0.1",
"strophe.js": "strophe/strophejs#44da5faca8baa61c691739d63af8b1dea1d2436c",
"strophe.js": "strophe/strophejs#f52f26e8cc23f738b7b39180a7ee4511ccd41526",
"twemoji": "^11.0.1",
"urijs": "^1.19.1"
}
......
......@@ -21,6 +21,18 @@ import sizzle from "sizzle";
*/
const u = {};
u.isTagEqual = function (stanza, name) {
if (stanza.nodeTree) {
return u.isTagEqual(stanza.nodeTree, name);
} else if (!(stanza instanceof Element)) {
throw Error(
"isTagEqual called with value which isn't "+
"an element or Strophe.Builder instance");
} else {
return Strophe.isTagEqual(stanza, name);
}
}
u.toStanza = function (string) {
return Strophe.xmlHtmlNode(string).firstElementChild;
}
......
......@@ -145,16 +145,22 @@
'<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">'+
'<required/>'+
'</bind>'+
`<sm xmlns='urn:xmpp:sm:3'/>`+
'<session xmlns="urn:ietf:params:xml:ns:xmpp-session">'+
'<optional/>'+
'</session>'+
'</stream:features>').firstChild;
c._proto._connect = function () {
c.authenticated = true;
c.connected = true;
c.mock = true;
c.jid = 'dummy@localhost/resource';
c._changeConnectStatus(Strophe.Status.BINDREQUIRED);
};
c.bind = function () {
c.authenticated = true;
this.authenticated = true;
c._changeConnectStatus(Strophe.Status.CONNECTED);
};
......@@ -180,7 +186,7 @@
_.forEach(spies.connection, method => spyOn(connection, method));
}
const _converse = await converse.initialize(_.extend({
const _converse = await converse.initialize(Object.assign({
'i18n': 'en',
'auto_subscribe': false,
'play_sounds': false,
......@@ -232,10 +238,8 @@
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
};
if (_.get(settings, 'auto_login') !== false) {
_converse.api.user.login({
'jid': 'dummy@localhost',
'password': 'secret'
});
_converse.api.user.login('dummy@localhost', 'secret');
await _converse.api.waitUntil('afterResourceBinding');
}
window.converse_disable_effects = true;
return _converse;
......
......@@ -44,6 +44,7 @@ var specs = [
"spec/protocol",
"spec/presence",
"spec/eventemitter",
"spec/smacks",
"spec/ping",
"spec/push",
"spec/xmppstatus",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment