Commit 9d77a4ef authored by JC Brand's avatar JC Brand

Fixes #129 Add support for XEP-0156.

Only XML is supported for now.
parent 54e9c51a
...@@ -2,11 +2,18 @@ ...@@ -2,11 +2,18 @@
## 6.0.0 (Unreleased) ## 6.0.0 (Unreleased)
- #129: Add support for XEP-0156: Disovering Alternative XMPP Connection Methods. Only XML is supported for now.
- #1691 Fix `collection.chatbox is undefined` errors - #1691 Fix `collection.chatbox is undefined` errors
- Prevent editing of sent file uploads. - Prevent editing of sent file uploads.
### Breaking changes ### Breaking changes
- In order to add support for XEP-0156, the XMPP connection needs to be created
only once we know the JID of the user that's logging in. This means that the
[connectionInitialized](https://conversejs.org/docs/html/api/-_converse.html#event:connectionInitialized)
event now fires much later than before. Plugins that rely on `connectionInitialized`
being triggered before the user's JID has been provided will need to be updated.
- The following API methods now return promises: - The following API methods now return promises:
* `_converse.api.chats.get` * `_converse.api.chats.get`
* `_converse.api.chats.create` * `_converse.api.chats.create`
......
...@@ -639,6 +639,23 @@ The default chat status that the user wil have. If you for example set this to ...@@ -639,6 +639,23 @@ The default chat status that the user wil have. If you for example set this to
``'chat'``, then Converse will send out a presence stanza with ``"show"`` ``'chat'``, then Converse will send out a presence stanza with ``"show"``
set to ``'chat'`` as soon as you've been logged in. set to ``'chat'`` as soon as you've been logged in.
discover_connection_methods
---------------------------
* Default: ``false``
Use `XEP-0156 <https://xmpp.org/extensions/xep-0156.html>`_ to discover whether
the XMPP host for the current user advertises any Websocket or BOSH connection
URLs that can be used.
If this is set to ``false``, then a `websocket_url`_ or `bosh_service_url`_ need to be
set.
Currently only the XML encoded host-meta resource is supported as shown in
`Example 2 under section 3.3 <https://xmpp.org/extensions/xep-0156.html#httpexamples>`_.
domain_placeholder domain_placeholder
------------------ ------------------
...@@ -647,8 +664,6 @@ domain_placeholder ...@@ -647,8 +664,6 @@ domain_placeholder
The placeholder text shown in the domain input on the registration form. The placeholder text shown in the domain input on the registration form.
emoji_image_path emoji_image_path
---------------- ----------------
...@@ -1624,6 +1639,7 @@ Allows you to show or hide buttons on the chatboxes' toolbars. ...@@ -1624,6 +1639,7 @@ Allows you to show or hide buttons on the chatboxes' toolbars.
.. _`websocket-url`: .. _`websocket-url`:
websocket_url websocket_url
------------- -------------
......
...@@ -404,7 +404,7 @@ ...@@ -404,7 +404,7 @@
it("can be retrieved from the XMPP server", mock.initConverse( it("can be retrieved from the XMPP server", mock.initConverse(
{'connection': ['send']}, ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {}, null, ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {},
async function (done, _converse) { async function (done, _converse) {
await test_utils.waitUntilDiscoConfirmed( await test_utils.waitUntilDiscoConfirmed(
...@@ -421,25 +421,12 @@ ...@@ -421,25 +421,12 @@
* </pubsub> * </pubsub>
* </iq> * </iq>
*/ */
let IQ_id; const IQ_stanzas = _converse.connection.IQ_stanzas;
const call = await u.waitUntil(() => const sent_stanza = await u.waitUntil(
_.filter( () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop());
_converse.connection.send.calls.all(),
call => {
const stanza = call.args[0];
if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
return;
}
if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
IQ_id = stanza.getAttribute('id');
return true;
}
}
).pop()
);
expect(Strophe.serialize(call.args[0])).toBe( expect(Strophe.serialize(sent_stanza)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="get" xmlns="jabber:client">`+ `<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+ '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<items node="storage:bookmarks"/>'+ '<items node="storage:bookmarks"/>'+
'</pubsub>'+ '</pubsub>'+
...@@ -469,7 +456,7 @@ ...@@ -469,7 +456,7 @@
expect(_converse.bookmarks.models.length).toBe(0); expect(_converse.bookmarks.models.length).toBe(0);
spyOn(_converse.bookmarks, 'onBookmarksReceived').and.callThrough(); spyOn(_converse.bookmarks, 'onBookmarksReceived').and.callThrough();
var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id}) var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')})
.c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'node': 'storage:bookmarks'}) .c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'}) .c('item', {'id': 'current'})
...@@ -495,7 +482,7 @@ ...@@ -495,7 +482,7 @@
describe("The rooms panel", function () { describe("The rooms panel", function () {
it("shows a list of bookmarks", mock.initConverse( it("shows a list of bookmarks", mock.initConverse(
{'connection': ['send']}, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},
async function (done, _converse) { async function (done, _converse) {
await test_utils.waitUntilDiscoConfirmed( await test_utils.waitUntilDiscoConfirmed(
...@@ -505,31 +492,19 @@ ...@@ -505,31 +492,19 @@
); );
test_utils.openControlBox(); test_utils.openControlBox();
let IQ_id; const IQ_stanzas = _converse.connection.IQ_stanzas;
const call = await u.waitUntil(() => const sent_stanza = await u.waitUntil(
_.filter( () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop());
_converse.connection.send.calls.all(),
call => { expect(Strophe.serialize(sent_stanza)).toBe(
const stanza = call.args[0]; `<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
return;
}
if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
IQ_id = stanza.getAttribute('id');
return true;
}
}
).pop()
);
expect(Strophe.serialize(call.args[0])).toBe(
`<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="get" xmlns="jabber:client">`+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+ '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<items node="storage:bookmarks"/>'+ '<items node="storage:bookmarks"/>'+
'</pubsub>'+ '</pubsub>'+
'</iq>' '</iq>'
); );
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id}) const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')})
.c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'node': 'storage:bookmarks'}) .c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'}) .c('item', {'id': 'current'})
...@@ -583,7 +558,7 @@ ...@@ -583,7 +558,7 @@
it("remembers the toggle state of the bookmarks list", mock.initConverse( it("remembers the toggle state of the bookmarks list", mock.initConverse(
{'connection': ['send']}, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},
async function (done, _converse) { async function (done, _converse) {
test_utils.openControlBox(); test_utils.openControlBox();
...@@ -593,31 +568,19 @@ ...@@ -593,31 +568,19 @@
['http://jabber.org/protocol/pubsub#publish-options'] ['http://jabber.org/protocol/pubsub#publish-options']
); );
let IQ_id; const IQ_stanzas = _converse.connection.IQ_stanzas;
const call = await u.waitUntil(() => const sent_stanza = await u.waitUntil(
_.filter( () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop());
_converse.connection.send.calls.all(),
call => { expect(Strophe.serialize(sent_stanza)).toBe(
const stanza = call.args[0]; `<iq from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
return;
}
if (sizzle('items[node="storage:bookmarks"]', stanza).length) {
IQ_id = stanza.getAttribute('id');
return true;
}
}
).pop()
);
expect(Strophe.serialize(call.args[0])).toBe(
`<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="get" xmlns="jabber:client">`+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+ '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<items node="storage:bookmarks"/>'+ '<items node="storage:bookmarks"/>'+
'</pubsub>'+ '</pubsub>'+
'</iq>' '</iq>'
); );
const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':IQ_id}) const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')})
.c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'node': 'storage:bookmarks'}) .c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'}) .c('item', {'id': 'current'})
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
it("contains a checkbox to indicate whether the computer is trusted or not", it("contains a checkbox to indicate whether the computer is trusted or not",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: false }, allow_registration: false },
async function (done, _converse) { async function (done, _converse) {
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
it("checkbox can be set to false by default", it("checkbox can be set to false by default",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
trusted: false, trusted: false,
allow_registration: false }, allow_registration: false },
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
it("is not available unless allow_registration=true", it("is not available unless allow_registration=true",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: false }, allow_registration: false },
async function (done, _converse) { async function (done, _converse) {
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
it("can be opened by clicking on the registration tab", it("can be opened by clicking on the registration tab",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: true }, allow_registration: true },
async function (done, _converse) { async function (done, _converse) {
...@@ -45,18 +45,18 @@ ...@@ -45,18 +45,18 @@
it("allows the user to choose an XMPP provider's domain", it("allows the user to choose an XMPP provider's domain",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: true }, allow_registration: true },
async function (done, _converse) { async function (done, _converse) {
spyOn(Strophe.Connection.prototype, 'connect');
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox(); test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
const registerview = cbview.registerpanel; const registerview = cbview.registerpanel;
spyOn(registerview, 'onProviderChosen').and.callThrough(); spyOn(registerview, 'onProviderChosen').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.connection, 'connect');
// Open the register panel // Open the register panel
cbview.el.querySelector('.toggle-register-login').click(); cbview.el.querySelector('.toggle-register-login').click();
...@@ -75,17 +75,18 @@ ...@@ -75,17 +75,18 @@
form.querySelector('input[name=domain]').value = 'conversejs.org'; form.querySelector('input[name=domain]').value = 'conversejs.org';
submit_button.click(); submit_button.click();
expect(registerview.onProviderChosen).toHaveBeenCalled(); expect(registerview.onProviderChosen).toHaveBeenCalled();
expect(_converse.connection.connect).toHaveBeenCalled(); await u.waitUntil(() => _converse.connection.connect.calls.count());
done(); done();
})); }));
it("will render a registration form as received from the XMPP provider", it("will render a registration form as received from the XMPP provider",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: true }, allow_registration: true },
async function (done, _converse) { async function (done, _converse) {
spyOn(Strophe.Connection.prototype, 'connect');
await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'));
test_utils.openControlBox(); test_utils.openControlBox();
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
...@@ -97,7 +98,6 @@ ...@@ -97,7 +98,6 @@
spyOn(registerview, 'onRegistrationFields').and.callThrough(); spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough(); spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.connection, 'connect').and.callThrough();
expect(registerview._registering).toBeFalsy(); expect(registerview._registering).toBeFalsy();
expect(_converse.connection.connected).toBeFalsy(); expect(_converse.connection.connected).toBeFalsy();
...@@ -105,7 +105,7 @@ ...@@ -105,7 +105,7 @@
registerview.el.querySelector('input[type=submit]').click(); registerview.el.querySelector('input[type=submit]').click();
expect(registerview.onProviderChosen).toHaveBeenCalled(); expect(registerview.onProviderChosen).toHaveBeenCalled();
expect(registerview._registering).toBeTruthy(); expect(registerview._registering).toBeTruthy();
expect(_converse.connection.connect).toHaveBeenCalled(); await u.waitUntil(() => _converse.connection.connect.calls.count());
let stanza = new Strophe.Builder("stream:features", { let stanza = new Strophe.Builder("stream:features", {
'xmlns:stream': "http://etherx.jabber.org/streams", 'xmlns:stream': "http://etherx.jabber.org/streams",
...@@ -137,7 +137,7 @@ ...@@ -137,7 +137,7 @@
it("will set form_type to legacy and submit it as legacy", it("will set form_type to legacy and submit it as legacy",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: true }, allow_registration: true },
async function (done, _converse) { async function (done, _converse) {
...@@ -194,7 +194,7 @@ ...@@ -194,7 +194,7 @@
it("will set form_type to xform and submit it as xform", it("will set form_type to xform and submit it as xform",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: true }, allow_registration: true },
async function (done, _converse) { async function (done, _converse) {
...@@ -267,7 +267,7 @@ ...@@ -267,7 +267,7 @@
it("renders the account registration form", it("renders the account registration form",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
view_mode: 'fullscreen', view_mode: 'fullscreen',
allow_registration: true }, allow_registration: true },
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
it("uses bookmarks to determine groupchat names", it("uses bookmarks to determine groupchat names",
mock.initConverse( mock.initConverse(
{'connection': ['send']}, null,
['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{'view_mode': 'fullscreen'}, {'view_mode': 'fullscreen'},
async function (done, _converse) { async function (done, _converse) {
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
describe("A groupchat shown in the groupchats list", function () { describe("A groupchat shown in the groupchats list", function () {
it("is highlighted if its currently open", mock.initConverse( it("is highlighted if it's currently open", mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], null, ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'],
{ view_mode: 'fullscreen', { view_mode: 'fullscreen',
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
...@@ -137,8 +137,6 @@ ...@@ -137,8 +137,6 @@
expect(room_els.length).toBe(1); expect(room_els.length).toBe(1);
item = room_els[0]; item = room_els[0];
expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit'); expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit');
const conv_el = document.querySelector('#conversejs');
conv_el.parentElement.removeChild(conv_el);
done(); done();
})); }));
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
it("gets enabled with an <enable> stanza and resumed with a <resume> stanza", it("gets enabled with an <enable> stanza and resumed with a <resume> stanza",
mock.initConverse( mock.initConverse(
null, ['connectionInitialized', 'chatBoxesInitialized'], null, ['chatBoxesInitialized'],
{ 'auto_login': false, { 'auto_login': false,
'enable_smacks': true, 'enable_smacks': true,
'show_controlbox_by_default': true, 'show_controlbox_by_default': true,
......
...@@ -1124,7 +1124,7 @@ converse.plugins.add('converse-chatview', { ...@@ -1124,7 +1124,7 @@ converse.plugins.add('converse-chatview', {
if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) { if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) {
_converse.router.navigate(''); _converse.router.navigate('');
} }
if (_converse.connection.connected) { if (_converse.api.connection.connected()) {
// Immediately sending the chat state, because the // Immediately sending the chat state, because the
// model is going to be destroyed afterwards. // model is going to be destroyed afterwards.
this.model.setChatState(_converse.INACTIVE); this.model.setChatState(_converse.INACTIVE);
......
...@@ -154,6 +154,7 @@ converse.plugins.add('converse-controlbox', { ...@@ -154,6 +154,7 @@ converse.plugins.add('converse-controlbox', {
_converse.api.promises.add('controlBoxInitialized'); _converse.api.promises.add('controlBoxInitialized');
const addControlBox = () => _converse.chatboxes.add({'id': 'controlbox'}); const addControlBox = () => _converse.chatboxes.add({'id': 'controlbox'});
_converse.ControlBox = _converse.ChatBox.extend({ _converse.ControlBox = _converse.ChatBox.extend({
...@@ -220,9 +221,9 @@ converse.plugins.add('converse-controlbox', { ...@@ -220,9 +221,9 @@ converse.plugins.add('converse-controlbox', {
} else { } else {
this.hide(); this.hide();
} }
if (!_converse.connection.connected ||
!_converse.connection.authenticated || const connection = get(_converse, 'connection', {});
_converse.connection.disconnecting) { if (!connection.connected || !connection.authenticated || connection.disconnecting) {
this.renderLoginPanel(); this.renderLoginPanel();
} else if (this.model.get('connected')) { } else if (this.model.get('connected')) {
this.renderControlBoxPane(); this.renderControlBoxPane();
...@@ -296,7 +297,8 @@ converse.plugins.add('converse-controlbox', { ...@@ -296,7 +297,8 @@ converse.plugins.add('converse-controlbox', {
if (_converse.sticky_controlbox) { if (_converse.sticky_controlbox) {
return; return;
} }
if (_converse.connection.connected && !_converse.connection.disconnecting) { const connection = get(_converse, 'connection', {});
if (connection.connected && !connection.disconnecting) {
this.model.save({'closed': true}); this.model.save({'closed': true});
} else { } else {
this.model.trigger('hide'); this.model.trigger('hide');
...@@ -319,7 +321,8 @@ converse.plugins.add('converse-controlbox', { ...@@ -319,7 +321,8 @@ converse.plugins.add('converse-controlbox', {
} }
u.addClass('hidden', this.el); u.addClass('hidden', this.el);
_converse.api.trigger('chatBoxClosed', this); _converse.api.trigger('chatBoxClosed', this);
if (!_converse.connection.connected) {
if (!_converse.api.connection.connected()) {
_converse.controlboxtoggle.render(); _converse.controlboxtoggle.render();
} }
_converse.controlboxtoggle.show(callback); _converse.controlboxtoggle.show(callback);
...@@ -464,7 +467,7 @@ converse.plugins.add('converse-controlbox', { ...@@ -464,7 +467,7 @@ converse.plugins.add('converse-controlbox', {
if (["converse/login", "converse/register"].includes(Backbone.history.getFragment())) { if (["converse/login", "converse/register"].includes(Backbone.history.getFragment())) {
_converse.router.navigate('', {'replace': true}); _converse.router.navigate('', {'replace': true});
} }
_converse.connection.reset(); _converse.connection && _converse.connection.reset();
_converse.api.user.login(jid, password); _converse.api.user.login(jid, password);
} }
}); });
...@@ -510,7 +513,7 @@ converse.plugins.add('converse-controlbox', { ...@@ -510,7 +513,7 @@ converse.plugins.add('converse-controlbox', {
// artifacts (i.e. on page load the toggle is shown only to then // artifacts (i.e. on page load the toggle is shown only to then
// seconds later be hidden in favor of the controlbox). // seconds later be hidden in favor of the controlbox).
this.el.innerHTML = tpl_controlbox_toggle({ this.el.innerHTML = tpl_controlbox_toggle({
'label_toggle': _converse.connection.connected ? __('Chat Contacts') : __('Toggle chat') 'label_toggle': _converse.api.connection.connected() ? __('Chat Contacts') : __('Toggle chat')
}) })
return this; return this;
}, },
...@@ -529,7 +532,7 @@ converse.plugins.add('converse-controlbox', { ...@@ -529,7 +532,7 @@ converse.plugins.add('converse-controlbox', {
if (!controlbox) { if (!controlbox) {
controlbox = addControlBox(); controlbox = addControlBox();
} }
if (_converse.connection.connected) { if (_converse.api.connection.connected()) {
controlbox.save({'closed': false}); controlbox.save({'closed': false});
} else { } else {
controlbox.trigger('show'); controlbox.trigger('show');
...@@ -540,7 +543,7 @@ converse.plugins.add('converse-controlbox', { ...@@ -540,7 +543,7 @@ converse.plugins.add('converse-controlbox', {
e.preventDefault(); e.preventDefault();
if (u.isVisible(_converse.root.querySelector("#controlbox"))) { if (u.isVisible(_converse.root.querySelector("#controlbox"))) {
const controlbox = _converse.chatboxes.get('controlbox'); const controlbox = _converse.chatboxes.get('controlbox');
if (_converse.connection.connected) { if (_converse.api.connection.connected) {
controlbox.save({closed: true}); controlbox.save({closed: true});
} else { } else {
controlbox.trigger('hide'); controlbox.trigger('hide');
...@@ -582,10 +585,9 @@ converse.plugins.add('converse-controlbox', { ...@@ -582,10 +585,9 @@ converse.plugins.add('converse-controlbox', {
}); });
Promise.all([
_converse.api.waitUntil('connectionInitialized'),
_converse.api.waitUntil('chatBoxViewsInitialized') _converse.api.waitUntil('chatBoxViewsInitialized')
]).then(addControlBox).catch(e => _converse.log(e, Strophe.LogLevel.FATAL)); .then(addControlBox)
.catch(e => _converse.log(e, Strophe.LogLevel.FATAL));
_converse.api.listen.on('chatBoxesFetched', () => { _converse.api.listen.on('chatBoxesFetched', () => {
const controlbox = _converse.chatboxes.get('controlbox') || addControlBox(); const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
......
...@@ -351,7 +351,7 @@ converse.plugins.add('converse-dragresize', { ...@@ -351,7 +351,7 @@ converse.plugins.add('converse-dragresize', {
_converse.resizing.chatbox.width, _converse.resizing.chatbox.width,
_converse.resizing.chatbox.model.get('default_width') _converse.resizing.chatbox.model.get('default_width')
); );
if (_converse.connection.connected) { if (_converse.api.connection.connected()) {
_converse.resizing.chatbox.model.save({'height': height}); _converse.resizing.chatbox.model.save({'height': height});
_converse.resizing.chatbox.model.save({'width': width}); _converse.resizing.chatbox.model.save({'width': width});
} else { } else {
......
...@@ -318,7 +318,7 @@ converse.plugins.add('converse-minimize', { ...@@ -318,7 +318,7 @@ converse.plugins.add('converse-minimize', {
* @param { _converse.ChatBoxView|_converse.ChatRoomView|_converse.ControlBoxView|_converse.HeadlinesBoxView } [newchat] * @param { _converse.ChatBoxView|_converse.ChatRoomView|_converse.ControlBoxView|_converse.HeadlinesBoxView } [newchat]
*/ */
async trimChats (newchat) { async trimChats (newchat) {
if (_converse.no_trimming || !_converse.connection.connected || _converse.view_mode !== 'overlayed') { if (_converse.no_trimming || !_converse.api.connection.connected() || _converse.view_mode !== 'overlayed') {
return; return;
} }
const shown_chats = this.getShownChats(); const shown_chats = this.getShownChats();
...@@ -556,10 +556,7 @@ converse.plugins.add('converse-minimize', { ...@@ -556,10 +556,7 @@ converse.plugins.add('converse-minimize', {
}); });
/************************ BEGIN Event Handlers ************************/ /************************ BEGIN Event Handlers ************************/
Promise.all([ _converse.api.waitUntil('chatBoxViewsInitialized').then(() => {
_converse.api.waitUntil('connectionInitialized'),
_converse.api.waitUntil('chatBoxViewsInitialized')
]).then(() => {
_converse.minimized_chats = new _converse.MinimizedChats({ _converse.minimized_chats = new _converse.MinimizedChats({
model: _converse.chatboxes model: _converse.chatboxes
}); });
......
...@@ -240,8 +240,8 @@ converse.plugins.add('converse-omemo', { ...@@ -240,8 +240,8 @@ converse.plugins.add('converse-omemo', {
/* The initialize function gets called as soon as the plugin is /* The initialize function gets called as soon as the plugin is
* loaded by Converse.js's plugin machinery. * loaded by Converse.js's plugin machinery.
*/ */
const { _converse } = this, const { _converse } = this;
{ __ } = _converse; const { __ } = _converse;
_converse.api.settings.update({ _converse.api.settings.update({
'omemo_default': false, 'omemo_default': false,
......
...@@ -175,7 +175,7 @@ converse.plugins.add('converse-register', { ...@@ -175,7 +175,7 @@ converse.plugins.add('converse-register', {
initialize () { initialize () {
this.reset(); this.reset();
this.registerHooks(); _converse.api.listen.on('connectionInitialized', () => this.registerHooks());
}, },
render () { render () {
...@@ -340,7 +340,7 @@ converse.plugins.add('converse-register', { ...@@ -340,7 +340,7 @@ converse.plugins.add('converse-register', {
* @method _converse.RegisterPanel#fetchRegistrationForm * @method _converse.RegisterPanel#fetchRegistrationForm
* @param { String } domain_name - XMPP server domain * @param { String } domain_name - XMPP server domain
*/ */
fetchRegistrationForm (domain_name) { async fetchRegistrationForm (domain_name) {
if (!this.model.get('registration_form_rendered')) { if (!this.model.get('registration_form_rendered')) {
this.renderRegistrationRequest(); this.renderRegistrationRequest();
} }
...@@ -348,7 +348,8 @@ converse.plugins.add('converse-register', { ...@@ -348,7 +348,8 @@ converse.plugins.add('converse-register', {
'domain': Strophe.getDomainFromJid(domain_name), 'domain': Strophe.getDomainFromJid(domain_name),
'_registering': true '_registering': true
}); });
_converse.connection.connect(this.domain, "", this.onConnectStatusChanged.bind(this)); await _converse.initConnection(this.domain);
_converse.connection.connect(this.domain, "", status => this.onConnectStatusChanged(status));
return false; return false;
}, },
......
...@@ -19,6 +19,10 @@ const BOSH_SESSION_ID = 'converse.bosh-session'; ...@@ -19,6 +19,10 @@ const BOSH_SESSION_ID = 'converse.bosh-session';
converse.plugins.add('converse-bosh', { converse.plugins.add('converse-bosh', {
enabled () {
return true;
},
initialize () { initialize () {
const { _converse } = this; const { _converse } = this;
...@@ -35,9 +39,15 @@ converse.plugins.add('converse-bosh', { ...@@ -35,9 +39,15 @@ converse.plugins.add('converse-bosh', {
_converse.bosh_session.browserStorage = new BrowserStorage.session(id); _converse.bosh_session.browserStorage = new BrowserStorage.session(id);
await new Promise(resolve => _converse.bosh_session.fetch({'success': resolve, 'error': resolve})); await new Promise(resolve => _converse.bosh_session.fetch({'success': resolve, 'error': resolve}));
} }
if (_converse.jid && _converse.bosh_session.get('jid') === _converse.jid) { if (_converse.jid) {
if (_converse.bosh_session.get('jid') !== _converse.jid) {
const jid = await _converse.setUserJID(_converse.jid);
_converse.bosh_session.clear({'silent': true }); _converse.bosh_session.clear({'silent': true });
_converse.bosh_session.save({'jid': _converse.jid, id}); _converse.bosh_session.save({jid});
}
} else { // Keepalive
const jid = _converse.bosh_session.get('jid');
jid && await _converse.setUserJID();
} }
return _converse.bosh_session; return _converse.bosh_session;
} }
...@@ -45,17 +55,17 @@ converse.plugins.add('converse-bosh', { ...@@ -45,17 +55,17 @@ converse.plugins.add('converse-bosh', {
_converse.startNewPreboundBOSHSession = function () { _converse.startNewPreboundBOSHSession = function () {
if (!_converse.prebind_url) { if (!_converse.prebind_url) {
throw new Error( throw new Error("startNewPreboundBOSHSession: If you use prebind then you MUST supply a prebind_url");
"attemptPreboundSession: If you use prebind then you MUST supply a prebind_url");
} }
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', _converse.prebind_url, true); xhr.open('GET', _converse.prebind_url, true);
xhr.setRequestHeader('Accept', 'application/json, text/javascript'); xhr.setRequestHeader('Accept', 'application/json, text/javascript');
xhr.onload = function () { xhr.onload = async function () {
if (xhr.status >= 200 && xhr.status < 400) { if (xhr.status >= 200 && xhr.status < 400) {
const data = JSON.parse(xhr.responseText); const data = JSON.parse(xhr.responseText);
const jid = await _converse.setUserJID(data.jid);
_converse.connection.attach( _converse.connection.attach(
data.jid, jid,
data.sid, data.sid,
data.rid, data.rid,
_converse.onConnectStatusChanged _converse.onConnectStatusChanged
...@@ -79,9 +89,6 @@ converse.plugins.add('converse-bosh', { ...@@ -79,9 +89,6 @@ converse.plugins.add('converse-bosh', {
_converse.restoreBOSHSession = async function () { _converse.restoreBOSHSession = async function () {
if (!_converse.api.connection.isType('bosh')) {
return false;
}
const jid = (await initBOSHSession()).get('jid'); const jid = (await initBOSHSession()).get('jid');
if (jid) { if (jid) {
try { try {
...@@ -119,9 +126,7 @@ converse.plugins.add('converse-bosh', { ...@@ -119,9 +126,7 @@ converse.plugins.add('converse-bosh', {
} }
}); });
_converse.api.listen.on('addClientFeatures', _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.BOSH));
() => _converse.api.disco.own.features.add(Strophe.NS.BOSH)
);
/************************ END Event Handlers ************************/ /************************ END Event Handlers ************************/
......
...@@ -230,6 +230,7 @@ _converse.default_settings = { ...@@ -230,6 +230,7 @@ _converse.default_settings = {
csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out. csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
debug: false, debug: false,
default_state: 'online', default_state: 'online',
discover_connection_methods: false,
geouri_regex: /https\:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g, geouri_regex: /https\:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g,
geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2', geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2',
idle_presence_timeout: 300, // Seconds after which an idle presence is sent idle_presence_timeout: 300, // Seconds after which an idle presence is sent
...@@ -330,7 +331,7 @@ function addPromise (promise) { ...@@ -330,7 +331,7 @@ function addPromise (promise) {
} }
_converse.isTestEnv = function () { _converse.isTestEnv = function () {
return _.get(_converse.connection, 'service') === 'jasmine tests'; return Strophe.Connection.name === 'MockConnection';
} }
...@@ -457,7 +458,7 @@ async function attemptNonPreboundSession (credentials, automatic) { ...@@ -457,7 +458,7 @@ async function attemptNonPreboundSession (credentials, automatic) {
} else if (!_converse.isTestEnv() && window.PasswordCredential) { } else if (!_converse.isTestEnv() && window.PasswordCredential) {
connect(await getLoginCredentialsFromBrowser()); connect(await getLoginCredentialsFromBrowser());
} else { } else {
throw new Error("attemptNonPreboundSession: Could not find any credentials to log you in with!"); _converse.log("attemptNonPreboundSession: Could not find any credentials to log in with", Strophe.LogLevel.WARN);
} }
} else if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(_converse.authentication) && (!automatic || _converse.auto_login)) { } else if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(_converse.authentication) && (!automatic || _converse.auto_login)) {
connect(); connect();
...@@ -523,9 +524,7 @@ function reconnect () { ...@@ -523,9 +524,7 @@ function reconnect () {
const debouncedReconnect = _.debounce(reconnect, 2000); const debouncedReconnect = _.debounce(reconnect, 2000);
_converse.shouldClearCache = function () { _converse.shouldClearCache = () => (!_converse.config.get('trusted') || _converse.isTestEnv());
return !_converse.config.get('trusted') || _converse.isTestEnv();
}
function clearSession () { function clearSession () {
if (_converse.session !== undefined) { if (_converse.session !== undefined) {
...@@ -548,15 +547,66 @@ function clearSession () { ...@@ -548,15 +547,66 @@ function clearSession () {
} }
/** async function onDomainDiscovered (response) {
* Creates a new Strophe.Connection instance and if applicable, attempt to const text = await response.text();
* restore the BOSH session or if `auto_login` is true, attempt to log in. const xrd = (new window.DOMParser()).parseFromString(text, "text/xml").firstElementChild;
if (xrd.nodeName != "XRD" || xrd.namespaceURI != "http://docs.oasis-open.org/ns/xri/xrd-1.0") {
return _converse.log("Could not discover XEP-0156 connection methods", Strophe.LogLevel.WARN);
}
const bosh_links = sizzle(`Link[rel="urn:xmpp:alt-connections:xbosh"]`, xrd);
const ws_links = sizzle(`Link[rel="urn:xmpp:alt-connections:websocket"]`, xrd);
const bosh_methods = bosh_links.map(el => el.getAttribute('href'));
const ws_methods = ws_links.map(el => el.getAttribute('href'));
// TODO: support multiple endpoints
_converse.websocket_url = ws_methods.pop();
_converse.bosh_service_url = bosh_methods.pop();
if (bosh_methods.length === 0 && ws_methods.length === 0) {
_converse.log(
"onDomainDiscovered: neither BOSH nor WebSocket connection methods have been specified with XEP-0156.",
Strophe.LogLevel.WARN
);
}
}
/* Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods.
*/ */
_converse.initConnection = async function () { async function discoverConnectionMethods (domain) {
if (!_converse.connection) { const options = {
if (!_converse.bosh_service_url && ! _converse.websocket_url) { 'mode': 'cors',
'headers': {
'Accept': 'application/xrd+xml, text/xml'
}
};
const url = `https://${domain}/.well-known/host-meta`;
let response;
try {
response = await fetch(url, options);
} catch (e) {
_converse.log(`Failed to discover alternative connection methods at ${url}`, Strophe.LogLevel.ERROR);
return _converse.log(e, Strophe.LogLevel.ERROR);
}
if (response.status >= 200 && response.status < 400) {
await onDomainDiscovered(response);
} else {
_converse.log("Could not discover XEP-0156 connection methods", Strophe.LogLevel.WARN);
}
}
_converse.initConnection = async function (domain) {
if (_converse.discover_connection_methods) {
await discoverConnectionMethods(domain);
}
if (! _converse.bosh_service_url) {
if (_converse.authentication === _converse.PREBIND) {
throw new Error("authentication is set to 'prebind' but we don't have a BOSH connection");
}
if (! _converse.websocket_url) {
throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."); throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both.");
} }
}
if (('WebSocket' in window || 'MozWebSocket' in window) && _converse.websocket_url) { if (('WebSocket' in window || 'MozWebSocket' in window) && _converse.websocket_url) {
_converse.connection = new Strophe.Connection( _converse.connection = new Strophe.Connection(
_converse.websocket_url, _converse.websocket_url,
...@@ -575,10 +625,6 @@ _converse.initConnection = async function () { ...@@ -575,10 +625,6 @@ _converse.initConnection = async function () {
throw new Error("initConnection: this browser does not support "+ throw new Error("initConnection: this browser does not support "+
"websockets and bosh_service_url wasn't specified."); "websockets and bosh_service_url wasn't specified.");
} }
if (_converse.auto_login || _converse.keepalive) {
await _converse.api.user.login(null, null, true);
}
}
setUpXMLLogging(); setUpXMLLogging();
/** /**
* Triggered once the `Strophe.Connection` constructor has been initialized, which * Triggered once the `Strophe.Connection` constructor has been initialized, which
...@@ -587,10 +633,10 @@ _converse.initConnection = async function () { ...@@ -587,10 +633,10 @@ _converse.initConnection = async function () {
* @event _converse#connectionInitialized * @event _converse#connectionInitialized
*/ */
_converse.api.trigger('connectionInitialized'); _converse.api.trigger('connectionInitialized');
}; }
async function setUserJID (jid) { async function initSession (jid) {
const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(); const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
const id = `converse.session-${bare_jid}`; const id = `converse.session-${bare_jid}`;
if (!_converse.session || _converse.session.get('id') !== id) { if (!_converse.session || _converse.session.get('id') !== id) {
...@@ -612,23 +658,14 @@ async function setUserJID (jid) { ...@@ -612,23 +658,14 @@ async function setUserJID (jid) {
} else { } else {
saveJIDtoSession(jid); saveJIDtoSession(jid);
} }
/**
* Triggered whenever the user's JID has been updated
* @event _converse#setUserJID
*/
_converse.api.trigger('setUserJID');
return jid;
} }
function saveJIDtoSession (jid) { function saveJIDtoSession (jid) {
jid = _converse.session.get('jid') || jid; jid = _converse.session.get('jid') || jid;
if (_converse.authentication !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) { if (_converse.authentication !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
jid = jid.toLowerCase() + _converse.generateResource(); jid = jid.toLowerCase() + _converse.generateResource();
} }
// Set JID on the connection object so that when we call
// `connection.bind` the new resource is found by Strophe.js
// and sent to the XMPP server.
_converse.connection.jid = jid;
_converse.jid = jid; _converse.jid = jid;
_converse.bare_jid = Strophe.getBareJidFromJid(jid); _converse.bare_jid = Strophe.getBareJidFromJid(jid);
_converse.resource = Strophe.getResourceFromJid(jid); _converse.resource = Strophe.getResourceFromJid(jid);
...@@ -640,6 +677,37 @@ function saveJIDtoSession (jid) { ...@@ -640,6 +677,37 @@ function saveJIDtoSession (jid) {
'domain': _converse.domain, 'domain': _converse.domain,
'active': true 'active': true
}); });
// Set JID on the connection object so that when we call `connection.bind`
// the new resource is found by Strophe.js and sent to the XMPP server.
_converse.connection.jid = jid;
}
/**
* Stores the passed in JID for the current user, potentially creating a
* resource if the JID is bare.
*
* Given that we can only create an XMPP connection if we know the domain of
* the server connect to and we only know this once we know the JID, we also
* call {@link _converse.initConnection } (if necessary) to make sure that the
* connection is set up.
*
* @method _converse#setUserJID
* @emits _converse#setUserJID
* @params { String } jid
*/
_converse.setUserJID = async function (jid) {
if (!_converse.connection || !u.isSameDomain(_converse.connection.jid, jid)) {
const domain = Strophe.getDomainFromJid(jid)
await _converse.initConnection(domain);
}
await initSession(jid);
/**
* Triggered whenever the user's JID has been updated
* @event _converse#setUserJID
*/
_converse.api.trigger('setUserJID');
return jid;
} }
...@@ -649,7 +717,7 @@ async function onConnected (reconnecting) { ...@@ -649,7 +717,7 @@ async function onConnected (reconnecting) {
*/ */
delete _converse.connection.reconnecting; delete _converse.connection.reconnecting;
_converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser _converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser
await setUserJID(_converse.connection.jid); await _converse.setUserJID(_converse.connection.jid);
/** /**
* Synchronous event triggered after we've sent an IQ to bind the * Synchronous event triggered after we've sent an IQ to bind the
* user's JID resource for this session. * user's JID resource for this session.
...@@ -681,9 +749,9 @@ function setUpXMLLogging () { ...@@ -681,9 +749,9 @@ function setUpXMLLogging () {
async function finishInitialization () { async function finishInitialization () {
initClientConfig(); initClientConfig();
initPlugins(); initPlugins();
await _converse.initConnection();
_converse.registerGlobalEventHandlers(); _converse.registerGlobalEventHandlers();
if (!Backbone.history.started) {
if (!Backbone.History.started) {
Backbone.history.start(); Backbone.history.start();
} }
if (_converse.idle_presence_timeout > 0) { if (_converse.idle_presence_timeout > 0) {
...@@ -691,6 +759,10 @@ async function finishInitialization () { ...@@ -691,6 +759,10 @@ async function finishInitialization () {
_converse.api.disco.own.features.add(Strophe.NS.IDLE); _converse.api.disco.own.features.add(Strophe.NS.IDLE);
}); });
} }
if (_converse.auto_login ||
_converse.keepalive && _.invoke(_converse.pluggable.plugins['converse-bosh'], 'enabled')) {
await _converse.api.user.login(null, null, true);
}
} }
...@@ -706,6 +778,7 @@ function finishDisconnection () { ...@@ -706,6 +778,7 @@ function finishDisconnection () {
_converse.connection.reset(); _converse.connection.reset();
tearDown(); tearDown();
clearSession(); clearSession();
delete _converse.connection;
/** /**
* Triggered after converse.js has disconnected from the XMPP server. * Triggered after converse.js has disconnected from the XMPP server.
* @event _converse#disconnected * @event _converse#disconnected
...@@ -725,7 +798,7 @@ function fetchLoginCredentials (wait=0) { ...@@ -725,7 +798,7 @@ function fetchLoginCredentials (wait=0) {
xhr.onload = () => { xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 400) { if (xhr.status >= 200 && xhr.status < 400) {
const data = JSON.parse(xhr.responseText); const data = JSON.parse(xhr.responseText);
setUserJID(data.jid).then(() => { _converse.setUserJID(data.jid).then(() => {
resolve({ resolve({
jid: data.jid, jid: data.jid,
password: data.password password: data.password
...@@ -761,7 +834,7 @@ async function getLoginCredentials () { ...@@ -761,7 +834,7 @@ async function getLoginCredentials () {
async function getLoginCredentialsFromBrowser () { async function getLoginCredentialsFromBrowser () {
const creds = await navigator.credentials.get({'password': true}); const creds = await navigator.credentials.get({'password': true});
if (creds && creds.type == 'password' && u.isValidJID(creds.id)) { if (creds && creds.type == 'password' && u.isValidJID(creds.id)) {
await setUserJID(creds.id); await _converse.setUserJID(creds.id);
return {'jid': creds.id, 'password': creds.password}; return {'jid': creds.id, 'password': creds.password};
} }
} }
...@@ -782,18 +855,20 @@ function cleanup () { ...@@ -782,18 +855,20 @@ function cleanup () {
if (_converse.chatboxviews) { if (_converse.chatboxviews) {
delete _converse.chatboxviews; delete _converse.chatboxviews;
} }
if (_converse.connection) {
_converse.connection.reset();
}
_converse.stopListening(); _converse.stopListening();
_converse.off(); _converse.off();
} }
_converse.initialize = async function (settings, callback) { _converse.initialize = async function (settings, callback) {
cleanup();
settings = settings !== undefined ? settings : {}; settings = settings !== undefined ? settings : {};
const init_promise = u.getResolveablePromise(); const init_promise = u.getResolveablePromise();
PROMISES.forEach(addPromise); PROMISES.forEach(addPromise);
if (_converse.connection !== undefined) {
cleanup();
}
if ('onpagehide' in window) { if ('onpagehide' in window) {
// Pagehide gets thrown in more cases than unload. Specifically it // Pagehide gets thrown in more cases than unload. Specifically it
...@@ -877,7 +952,7 @@ _converse.initialize = async function (settings, callback) { ...@@ -877,7 +952,7 @@ _converse.initialize = async function (settings, callback) {
if (_converse.idle_seconds > 0) { if (_converse.idle_seconds > 0) {
_converse.idle_seconds = 0; _converse.idle_seconds = 0;
} }
if (!_converse.connection.authenticated) { if (!_.get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection. // We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects. // This can happen when the connection reconnects.
return; return;
...@@ -901,7 +976,7 @@ _converse.initialize = async function (settings, callback) { ...@@ -901,7 +976,7 @@ _converse.initialize = async function (settings, callback) {
/* An interval handler running every second. /* An interval handler running every second.
* Used for CSI and the auto_away and auto_xa features. * Used for CSI and the auto_away and auto_xa features.
*/ */
if (!_converse.connection.authenticated) { if (!_.get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection. // We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects. // This can happen when the connection reconnects.
return; return;
...@@ -1378,7 +1453,7 @@ _converse.api = { ...@@ -1378,7 +1453,7 @@ _converse.api = {
* @returns {boolean} Whether there is an established connection or not. * @returns {boolean} Whether there is an established connection or not.
*/ */
connected () { connected () {
return (_converse.connection && _converse.connection.connected) || false; return _.get(_converse, 'connection', {}).connected && true;
}, },
/** /**
...@@ -1417,7 +1492,7 @@ _converse.api = { ...@@ -1417,7 +1492,7 @@ _converse.api = {
// We also call `_proto._doDisconnect` so that connection event handlers // We also call `_proto._doDisconnect` so that connection event handlers
// for the old transport are removed. // for the old transport are removed.
if (_converse.api.connection.isType('websocket') && _converse.bosh_service_url) { if (_converse.api.connection.isType('websocket') && _converse.bosh_service_url) {
await setUserJID(_converse.bare_jid); await _converse.setUserJID(_converse.bare_jid);
_converse.connection._proto._doDisconnect(); _converse.connection._proto._doDisconnect();
_converse.connection._proto = new Strophe.Bosh(_converse.connection); _converse.connection._proto = new Strophe.Bosh(_converse.connection);
_converse.connection.service = _converse.bosh_service_url; _converse.connection.service = _converse.bosh_service_url;
...@@ -1426,9 +1501,9 @@ _converse.api = { ...@@ -1426,9 +1501,9 @@ _converse.api = {
// When reconnecting anonymously, we need to connect with only // When reconnecting anonymously, we need to connect with only
// the domain, not the full JID that we had in our previous // the domain, not the full JID that we had in our previous
// (now failed) session. // (now failed) session.
await setUserJID(_converse.settings.jid); await _converse.setUserJID(_converse.settings.jid);
} else { } else {
await setUserJID(_converse.bare_jid); await _converse.setUserJID(_converse.bare_jid);
} }
_converse.connection._proto._doDisconnect(); _converse.connection._proto._doDisconnect();
_converse.connection._proto = new Strophe.Websocket(_converse.connection); _converse.connection._proto = new Strophe.Websocket(_converse.connection);
...@@ -1439,7 +1514,7 @@ _converse.api = { ...@@ -1439,7 +1514,7 @@ _converse.api = {
// When reconnecting anonymously, we need to connect with only // When reconnecting anonymously, we need to connect with only
// the domain, not the full JID that we had in our previous // the domain, not the full JID that we had in our previous
// (now failed) session. // (now failed) session.
await setUserJID(_converse.settings.jid); await _converse.setUserJID(_converse.settings.jid);
} }
if (_converse.connection.reconnecting) { if (_converse.connection.reconnecting) {
debouncedReconnect(); debouncedReconnect();
...@@ -1527,20 +1602,19 @@ _converse.api = { ...@@ -1527,20 +1602,19 @@ _converse.api = {
* fails to restore a previous auth'd session. * fails to restore a previous auth'd session.
*/ */
async login (jid, password, automatic=false) { async login (jid, password, automatic=false) {
if (_converse.api.connection.isType('bosh')) { if (jid || _converse.jid) {
jid = await _converse.setUserJID(jid || _converse.jid);
}
// See whether there is a BOSH session to re-attach to
if (_.invoke(_converse.pluggable.plugins['converse-bosh'], 'enabled')) {
if (await _converse.restoreBOSHSession()) { if (await _converse.restoreBOSHSession()) {
return; return;
} else if (_converse.authentication === _converse.PREBIND && (!automatic || _converse.auto_login)) { } else if (_converse.authentication === _converse.PREBIND && (!automatic || _converse.auto_login)) {
return _converse.startNewPreboundBOSHSession(); return _converse.startNewPreboundBOSHSession();
} }
} else if (_converse.authentication === _converse.PREBIND) {
throw new Error("authentication is set to 'prebind' but we don't have a BOSH connection");
} }
if (jid || _converse.jid) {
// Reassign because we might have gained a resource
jid = await setUserJID(jid || _converse.jid);
}
password = password || _converse.password; password = password || _converse.password;
const credentials = (jid && password) ? { jid, password } : null; const credentials = (jid && password) ? { jid, password } : null;
attemptNonPreboundSession(credentials, automatic); attemptNonPreboundSession(credentials, automatic);
...@@ -1866,6 +1940,11 @@ _converse.api = { ...@@ -1866,6 +1940,11 @@ _converse.api = {
* _converse.api.send(msg); * _converse.api.send(msg);
*/ */
send (stanza) { send (stanza) {
if (!_converse.api.connection.connected()) {
_converse.log("Not sending stanza because we're not connected!", Strophe.LogLevel.WARN);
_converse.log(Strophe.serialize(stanza), Strophe.LogLevel.WARN);
return;
}
if (_.isString(stanza)) { if (_.isString(stanza)) {
stanza = u.toStanza(stanza); stanza = u.toStanza(stanza);
} }
......
...@@ -633,7 +633,7 @@ converse.plugins.add('converse-muc', { ...@@ -633,7 +633,7 @@ converse.plugins.add('converse-muc', {
disco_entity.destroy(); disco_entity.destroy();
} }
} }
if (_converse.connection.connected) { if (_converse.api.connection.connected()) {
this.sendUnavailablePresence(exit_msg); this.sendUnavailablePresence(exit_msg);
} }
u.safeSave(this, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}); u.safeSave(this, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
......
...@@ -107,6 +107,15 @@ u.isSameBareJID = function (jid1, jid2) { ...@@ -107,6 +107,15 @@ u.isSameBareJID = function (jid1, jid2) {
Strophe.getBareJidFromJid(jid2).toLowerCase(); Strophe.getBareJidFromJid(jid2).toLowerCase();
}; };
u.isSameDomain = function (jid1, jid2) {
if (!_.isString(jid1) || !_.isString(jid2)) {
return false;
}
return Strophe.getDomainFromJid(jid1).toLowerCase() ===
Strophe.getDomainFromJid(jid2).toLowerCase();
};
u.isNewMessage = function (message) { u.isNewMessage = function (message) {
/* Given a stanza, determine whether it's a new /* Given a stanza, determine whether it's a new
* message, i.e. not a MAM archived one. * message, i.e. not a MAM archived one.
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
const Strophe = converse.env.Strophe; const Strophe = converse.env.Strophe;
const dayjs = converse.env.dayjs; const dayjs = converse.env.dayjs;
const $iq = converse.env.$iq; const $iq = converse.env.$iq;
const u = converse.env.utils;
window.libsignal = { window.libsignal = {
'SignalProtocolAddress': function (name, device_id) { 'SignalProtocolAddress': function (name, device_id) {
...@@ -32,7 +31,7 @@ ...@@ -32,7 +31,7 @@
return Promise.resolve(key_and_tag); return Promise.resolve(key_and_tag);
} }
}, },
'SessionBuilder': function (storage, remote_address) { 'SessionBuilder': function (storage, remote_address) { // eslint-disable-line no-unused-vars
this.processPreKey = function () { this.processPreKey = function () {
return Promise.resolve(); return Promise.resolve();
} }
...@@ -116,15 +115,18 @@ ...@@ -116,15 +115,18 @@
'preventDefault': function () {} 'preventDefault': function () {}
}; };
mock.mock_connection = function () { // eslint-disable-line wrap-iife
return function () { const OriginalConnection = Strophe.Connection;
function MockConnection (service, options) {
OriginalConnection.call(this, service, options);
Strophe.Bosh.prototype._processRequest = function () {}; // Don't attempt to send out stanzas Strophe.Bosh.prototype._processRequest = function () {}; // Don't attempt to send out stanzas
const c = new Strophe.Connection('jasmine tests'); const sendIQ = this.sendIQ;
const sendIQ = c.sendIQ;
c.IQ_stanzas = []; this.IQ_stanzas = [];
c.IQ_ids = []; this.IQ_ids = [];
c.sendIQ = function (iq, callback, errback) { this.sendIQ = function (iq, callback, errback) {
if (!_.isElement(iq)) { if (!_.isElement(iq)) {
iq = iq.nodeTree; iq = iq.nodeTree;
} }
...@@ -134,9 +136,9 @@ ...@@ -134,9 +136,9 @@
return id; return id;
} }
const send = c.send; const send = this.send;
c.sent_stanzas = []; this.sent_stanzas = [];
c.send = function (stanza) { this.send = function (stanza) {
if (_.isElement(stanza)) { if (_.isElement(stanza)) {
this.sent_stanzas.push(stanza); this.sent_stanzas.push(stanza);
} else { } else {
...@@ -145,11 +147,11 @@ ...@@ -145,11 +147,11 @@
return send.apply(this, arguments); return send.apply(this, arguments);
} }
c.features = Strophe.xmlHtmlNode( this.features = Strophe.xmlHtmlNode(
'<stream:features xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">'+ '<stream:features xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">'+
'<ver xmlns="urn:xmpp:features:rosterver"/>'+ '<ver xmlns="urn:xmpp:features:rosterver"/>'+
'<csi xmlns="urn:xmpp:csi:0"/>'+ '<csi xmlns="urn:xmpp:csi:0"/>'+
'<c xmlns="http://jabber.org/protocol/caps" ver="UwBpfJpEt3IoLYfWma/o/p3FFRo=" hash="sha-1" node="http://prosody.im"/>'+ '<this xmlns="http://jabber.org/protocol/caps" ver="UwBpfJpEt3IoLYfWma/o/p3FFRo=" hash="sha-1" node="http://prosody.im"/>'+
'<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">'+ '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">'+
'<required/>'+ '<required/>'+
'</bind>'+ '</bind>'+
...@@ -159,52 +161,41 @@ ...@@ -159,52 +161,41 @@
'</session>'+ '</session>'+
'</stream:features>').firstChild; '</stream:features>').firstChild;
c._proto._connect = function () { this._proto._connect = () => {
c.connected = true; this.connected = true;
c.mock = true; this.mock = true;
c.jid = 'romeo@montague.lit/orchard'; this.jid = 'romeo@montague.lit/orchard';
c._changeConnectStatus(Strophe.Status.BINDREQUIRED); this._changeConnectStatus(Strophe.Status.BINDREQUIRED);
}; };
c.bind = function () { this.bind = () => {
c.authenticated = true;
this.authenticated = true; this.authenticated = true;
c._changeConnectStatus(Strophe.Status.CONNECTED); this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED);
}; };
c._proto._disconnect = function () { this._proto._disconnect = () => this._onDisconnectTimeout();
c._onDisconnectTimeout(); this._proto._onDisconnectTimeout = _.noop;
} }
c._proto._onDisconnectTimeout = _.noop; MockConnection.prototype = Object.create(OriginalConnection.prototype);
return c; Strophe.Connection = MockConnection;
};
}();
async function initConverse (settings, spies={}, promises) { async function initConverse (settings, spies={}) {
window.localStorage.clear(); window.localStorage.clear();
window.sessionStorage.clear(); window.sessionStorage.clear();
const el = document.querySelector('#conversejs');
if (el) {
el.parentElement.removeChild(el);
}
const connection = mock.mock_connection();
if (spies && spies.connection) {
spies.connection.forEach(method => spyOn(connection, method));
}
const _converse = await converse.initialize(Object.assign({ const _converse = await converse.initialize(Object.assign({
'i18n': 'en', 'animate': false,
'auto_subscribe': false, 'auto_subscribe': false,
'play_sounds': false,
'bosh_service_url': 'montague.lit/http-bind', 'bosh_service_url': 'montague.lit/http-bind',
'connection': connection, 'debug': false,
'animate': false, 'i18n': 'en',
'use_emojione': false,
'no_trimming': true, 'no_trimming': true,
'play_sounds': false,
'use_emojione': false,
'view_mode': mock.view_mode, 'view_mode': mock.view_mode,
'debug': false
}, settings || {})); }, settings || {}));
if (spies && spies._converse) { if (spies && spies._converse) {
...@@ -214,7 +205,7 @@ ...@@ -214,7 +205,7 @@
_converse.ChatBoxViews.prototype.trimChat = function () {}; _converse.ChatBoxViews.prototype.trimChat = function () {};
_converse.api.vcard.get = function (model, force) { _converse.api.vcard.get = function (model, force) {
return new Promise((resolve, reject) => { return new Promise(resolve => {
let jid; let jid;
if (_.isString(model)) { if (_.isString(model)) {
jid = model; jid = model;
...@@ -263,9 +254,13 @@ ...@@ -263,9 +254,13 @@
return async done => { return async done => {
const _converse = await initConverse(settings, spies); const _converse = await initConverse(settings, spies);
async function _done () { async function _done () {
if (_converse.api.connection.connected()) {
await _converse.api.user.logout(); await _converse.api.user.logout();
}
const el = document.querySelector('#conversejs'); const el = document.querySelector('#conversejs');
if (el) {
el.parentElement.removeChild(el); el.parentElement.removeChild(el);
}
done(); done();
} }
await Promise.all((promise_names || []).map(_converse.api.waitUntil)); await Promise.all((promise_names || []).map(_converse.api.waitUntil));
......
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