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
This commit is contained in:
JC Brand 2018-05-29 12:00:23 +02:00
parent a46ee4dfe1
commit 7b11d85503
24 changed files with 764 additions and 268 deletions

View File

@ -4,7 +4,7 @@ cache:
directories:
- node_modules
addons:
chrome: unstable
chrome: stable
node_js:
- "10"
install: make stamp-npm

View File

@ -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)

View File

@ -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
});

View File

@ -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
-----------

View File

@ -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>`_.
@ -159,7 +141,7 @@ is configured depends on what webserver is used for your file upload server.
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
@ -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

4
package-lock.json generated
View File

@ -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",

View File

@ -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;

View File

@ -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);
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';
cbview.el.querySelector('input[name="jid"]').value = 'dummy@localhost';
cbview.el.querySelector('input[name="password"]').value = 'secret';
spyOn(cbview.loginpanel, 'connect');
cbview.delegateEvents();
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();
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",

View File

@ -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();

View File

@ -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();

128
spec/smacks.js Normal file
View File

@ -0,0 +1,128 @@
(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();
}));
});
}));

View File

@ -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);
}
});

View File

@ -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();
}
},

View File

@ -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) {

View File

@ -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);

View File

@ -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");
}

View File

@ -0,0 +1,242 @@
// 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);
}
});

View File

@ -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']

View File

@ -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 */

View File

@ -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"
}

View File

@ -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;
}

View File

@ -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;

View File

@ -44,6 +44,7 @@ var specs = [
"spec/protocol",
"spec/presence",
"spec/eventemitter",
"spec/smacks",
"spec/ping",
"spec/push",
"spec/xmppstatus",