Add experimental support for running the XMPP conneciton inside a shared worker

Still lacks inter-tab communication to update state across tabs, i.e.
when sending a 1-on-1 message in one tab, it doesn't appear in another,
because that information is not available via the websocket connection.

- Create a new `Connection` class that extends Strophe.Connection and
    move related code from `converse-core.js` into this class.
- Store the session in localStorage when using a worker
- Move XEP-0156 code to connection.js
    This allows us to initialize the connection without needing to know the
    domain.
This commit is contained in:
JC Brand 2020-06-03 17:59:41 +02:00
parent 5e479d3bbb
commit 16ca8044f8
17 changed files with 605 additions and 557 deletions

View File

@ -578,6 +578,9 @@ For documentation on the configuration options that ``Strophe.Connection``
accepts, refer to the accepts, refer to the
`Strophe.Connection documentation <http://strophe.im/strophejs/doc/1.2.8/files/strophe-js.html#Strophe.Connection.Strophe.Connection>`_. `Strophe.Connection documentation <http://strophe.im/strophejs/doc/1.2.8/files/strophe-js.html#Strophe.Connection.Strophe.Connection>`_.
Restricting the supported authentication mechanisms:
****************************************************
As an example, suppose you want to restrict the supported SASL authentication As an example, suppose you want to restrict the supported SASL authentication
mechanisms, then you'd pass in the ``mechanisms`` as a ``connection_options`` mechanisms, then you'd pass in the ``mechanisms`` as a ``connection_options``
``key:value`` pair: ``key:value`` pair:
@ -589,9 +592,29 @@ mechanisms, then you'd pass in the ``mechanisms`` as a ``connection_options``
'mechanisms': [ 'mechanisms': [
converse.env.Strophe.SASLMD5, converse.env.Strophe.SASLMD5,
] ]
}, }
}); });
Running the XMPP Connection inside a shared worker
**************************************************
Newer versions of Strophe.js, support the ability to run the XMPP Connection
inside a `shared worker <https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker>`_ that's shared
between open tabs in the browser in which Converse is running (and which have the same domain).
*Note:* This feature is experimental and there currently is no way to
synchronize actions between tabs. For example, sent 1-on-1 messages aren't
reflected by the server, so you if you send such a message in one tab, it won't
appear in another.
.. code-block:: javascript
converse.initialize({
connection_options: { 'worker': true }
});
.. _`credentials_url`: .. _`credentials_url`:
credentials_url credentials_url

119
package-lock.json generated
View File

@ -3283,9 +3283,12 @@
} }
}, },
"strophe.js": { "strophe.js": {
"version": "1.3.4", "version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.3.4.tgz", "from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"integrity": "sha512-jSLDG8jolhAwGOSgiJ7DTMSYK3wVoEJHKtpVRyEacQZ6CWA6z2WRPJpcFMjsIweq5aP9/XIvKUQqHBu/ZhvESA==" "requires": {
"abab": "^2.0.3",
"xmldom": "^0.1.27"
}
}, },
"twemoji": { "twemoji": {
"version": "12.1.5", "version": "12.1.5",
@ -4901,30 +4904,27 @@
} }
}, },
"@octokit/endpoint": { "@octokit/endpoint": {
"version": "6.0.3", "version": "6.0.5",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.5.tgz",
"integrity": "sha512-Y900+r0gIz+cWp6ytnkibbD95ucEzDSKzlEnaWS52hbCDNcCJYO5mRmWW7HRAnDc7am+N/5Lnd8MppSaTYx1Yg==", "integrity": "sha512-70K5u6zd45ItOny6aHQAsea8HHQjlQq85yqOMe+Aj8dkhN2qSJ9T+Q3YjUjEYfPRBcuUWNgMn62DQnP/4LAIiQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@octokit/types": "^5.0.0", "@octokit/types": "^5.0.0",
"is-plain-object": "^3.0.0", "is-plain-object": "^4.0.0",
"universal-user-agent": "^5.0.0" "universal-user-agent": "^6.0.0"
}, },
"dependencies": { "dependencies": {
"is-plain-object": { "is-plain-object": {
"version": "3.0.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-4.1.1.tgz",
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", "integrity": "sha512-5Aw8LLVsDlZsETVMhoMXzqsXwQqr/0vlnBYzIXJbYo2F4yYlhLHs+Ez7Bod7IIQKWkJbJfxrWD7pA1Dw1TKrwA==",
"dev": true "dev": true
}, },
"universal-user-agent": { "universal-user-agent": {
"version": "5.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
"dev": true, "dev": true
"requires": {
"os-name": "^3.1.0"
}
} }
} }
}, },
@ -4982,19 +4982,19 @@
} }
}, },
"@octokit/request": { "@octokit/request": {
"version": "5.4.5", "version": "5.4.7",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.5.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.7.tgz",
"integrity": "sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg==", "integrity": "sha512-FN22xUDP0i0uF38YMbOfx6TotpcENP5W8yJM1e/LieGXn6IoRxDMnBf7tx5RKSW4xuUZ/1P04NFZy5iY3Rax1A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@octokit/endpoint": "^6.0.1", "@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.0.0", "@octokit/request-error": "^2.0.0",
"@octokit/types": "^5.0.0", "@octokit/types": "^5.0.0",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"is-plain-object": "^3.0.0", "is-plain-object": "^4.0.0",
"node-fetch": "^2.3.0", "node-fetch": "^2.3.0",
"once": "^1.4.0", "once": "^1.4.0",
"universal-user-agent": "^5.0.0" "universal-user-agent": "^6.0.0"
}, },
"dependencies": { "dependencies": {
"@octokit/request-error": { "@octokit/request-error": {
@ -5009,19 +5009,16 @@
} }
}, },
"is-plain-object": { "is-plain-object": {
"version": "3.0.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-4.1.1.tgz",
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", "integrity": "sha512-5Aw8LLVsDlZsETVMhoMXzqsXwQqr/0vlnBYzIXJbYo2F4yYlhLHs+Ez7Bod7IIQKWkJbJfxrWD7pA1Dw1TKrwA==",
"dev": true "dev": true
}, },
"universal-user-agent": { "universal-user-agent": {
"version": "5.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
"dev": true, "dev": true
"requires": {
"os-name": "^3.1.0"
}
} }
} }
}, },
@ -5072,9 +5069,9 @@
} }
}, },
"@octokit/types": { "@octokit/types": {
"version": "5.0.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.1.2.tgz",
"integrity": "sha512-GorvORVwp244fGKEt3cgt/P+M0MGy4xEDbckw+K5ojEezxyMDgCaYPKVct+/eWQfZXOT7uq0xRpmrl/+hliabA==", "integrity": "sha512-+zuMnja97vuZmWa+HdUY+0KB9MLwcEHueSSyKu0G/HqZaFYCVdLpBkavb0xyDlH7eoBdvAvSX/+Y8+4FOEZkrQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": ">= 8" "@types/node": ">= 8"
@ -7558,19 +7555,6 @@
"through2": "^3.0.0" "through2": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"handlebars": {
"version": "4.7.6",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz",
"integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==",
"dev": true,
"requires": {
"minimist": "^1.2.5",
"neo-async": "^2.6.0",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0"
}
},
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -7583,12 +7567,6 @@
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true "dev": true
}, },
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"through2": { "through2": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
@ -9197,9 +9175,9 @@
"dev": true "dev": true
}, },
"envinfo": { "envinfo": {
"version": "7.5.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.5.1.tgz", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.7.2.tgz",
"integrity": "sha512-hQBkDf2iO4Nv0CNHpCuSBeaSrveU6nThVxFGTrq/eDlV716UQk09zChaJae4mZRsos1x4YLY2TaH3LHUae3ZmQ==", "integrity": "sha512-k3Eh5bKuQnZjm49/L7H4cHzs2FlL5QjbTB3JrPxoTI8aJG7hVMe4uKyJxSYH4ahseby2waUwk5OaKX/nAsaYgg==",
"dev": true "dev": true
}, },
"err-code": { "err-code": {
@ -13757,9 +13735,9 @@
} }
}, },
"localforage": { "localforage": {
"version": "1.7.4", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.4.tgz", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.8.1.tgz",
"integrity": "sha512-3EmVZatmNVeCo/t6Te7P06h2alGwbq8wXlSkcSXMvDE2/edPmsVqTPlzGnZaqwZZDBs6v+kxWpqjVsqsNJT8jA==", "integrity": "sha512-azSSJJfc7h4bVpi0PGi+SmLQKJl2/8NErI+LhJsrORNikMZnhaQ7rv9fHj+ofwgSHrKRlsDCL/639a6nECIKuQ==",
"requires": { "requires": {
"lie": "3.1.1" "lie": "3.1.1"
} }
@ -13965,9 +13943,9 @@
} }
}, },
"macos-release": { "macos-release": {
"version": "2.4.0", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.0.tgz", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz",
"integrity": "sha512-ko6deozZYiAkqa/0gmcsz+p4jSy3gY7/ZsCEokPaYd8k+6/aXGkiTgr61+Owup7Sf+xjqW8u2ElhoM9SEcEfuA==", "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==",
"dev": true "dev": true
}, },
"make-dir": { "make-dir": {
@ -14195,9 +14173,9 @@
} }
}, },
"parse-json": { "parse-json": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz",
"integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "^7.0.0",
@ -22874,11 +22852,10 @@
} }
}, },
"strophe.js": { "strophe.js": {
"version": "1.3.6", "version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.3.6.tgz", "from": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"integrity": "sha512-kTFdf6ziHqlp2PCr7Z7D/lhO+Hd0FIhzwXXlAIQNOqCWwnnTEor9folIUCVoXgZRMYPQ9BTXI2wBv88RG8mgAA==",
"requires": { "requires": {
"abab": "^2.0.0", "abab": "^2.0.3",
"ws": "^7.0.0", "ws": "^7.0.0",
"xmldom": "^0.1.27" "xmldom": "^0.1.27"
}, },

View File

@ -1,4 +1,4 @@
/* global mock */ /* global mock, converse */
describe("A chat room", function () { describe("A chat room", function () {

View File

@ -599,74 +599,6 @@ window.addEventListener('converse-loaded', () => {
'preventDefault': function () {} 'preventDefault': 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
const sendIQ = this.sendIQ;
this.IQ_stanzas = [];
this.IQ_ids = [];
this.sendIQ = function (iq, callback, errback) {
if (!_.isElement(iq)) {
iq = iq.nodeTree;
}
this.IQ_stanzas.push(iq);
const id = sendIQ.bind(this)(iq, callback, errback);
this.IQ_ids.push(id);
return id;
}
const send = this.send;
this.sent_stanzas = [];
this.send = function (stanza) {
if (_.isElement(stanza)) {
this.sent_stanzas.push(stanza);
} else {
this.sent_stanzas.push(stanza.nodeTree);
}
return send.apply(this, arguments);
}
this.features = Strophe.xmlHtmlNode(
'<stream:features xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">'+
'<ver xmlns="urn:xmpp:features:rosterver"/>'+
'<csi xmlns="urn:xmpp:csi:0"/>'+
'<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">'+
'<required/>'+
'</bind>'+
`<sm xmlns='urn:xmpp:sm:3'/>`+
'<session xmlns="urn:ietf:params:xml:ns:xmpp-session">'+
'<optional/>'+
'</session>'+
'</stream:features>').firstChild;
this._proto._connect = () => {
this.connected = true;
this.mock = true;
this.jid = 'romeo@montague.lit/orchard';
this._changeConnectStatus(Strophe.Status.BINDREQUIRED);
};
this.bind = () => {
this.authenticated = true;
this.authenticated = true;
if (!_converse.no_connection_on_bind) {
this._changeConnectStatus(Strophe.Status.CONNECTED);
}
};
this._proto._disconnect = () => this._onDisconnectTimeout();
this._proto._onDisconnectTimeout = _.noop;
}
MockConnection.prototype = Object.create(OriginalConnection.prototype);
Strophe.Connection = MockConnection;
function clearIndexedDB () { function clearIndexedDB () {
const promise = u.getResolveablePromise(); const promise = u.getResolveablePromise();
const db_request = window.indexedDB.open("converse-test-persistent"); const db_request = window.indexedDB.open("converse-test-persistent");

View File

@ -1,4 +1,4 @@
/*global mock */ /*global mock, converse */
const Strophe = converse.env.Strophe; const Strophe = converse.env.Strophe;
const $iq = converse.env.$iq; const $iq = converse.env.$iq;
@ -56,7 +56,6 @@ describe("The Registration Panel", function () {
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'));
const toggle = document.querySelector(".toggle-controlbox"); const toggle = document.querySelector(".toggle-controlbox");
@ -66,6 +65,7 @@ describe("The Registration Panel", function () {
await u.waitUntil(() => u.isVisible(cbview.el)); await u.waitUntil(() => u.isVisible(cbview.el));
const registerview = cbview.registerpanel; const registerview = cbview.registerpanel;
spyOn(registerview, 'onProviderChosen').and.callThrough(); spyOn(registerview, 'onProviderChosen').and.callThrough();
spyOn(registerview, 'fetchRegistrationForm').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
// Open the register panel // Open the register panel
@ -85,7 +85,8 @@ describe("The Registration Panel", function () {
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();
await u.waitUntil(() => _converse.connection.connect.calls.count()); expect(registerview.fetchRegistrationForm).toHaveBeenCalled();
delete _converse.connection;
done(); done();
})); }));
@ -97,12 +98,12 @@ describe("The Registration Panel", function () {
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'));
const cbview = _converse.api.controlbox.get(); const cbview = _converse.api.controlbox.get();
cbview.el.querySelector('.toggle-register-login').click(); cbview.el.querySelector('.toggle-register-login').click();
const registerview = _converse.chatboxviews.get('controlbox').registerpanel; const registerview = _converse.chatboxviews.get('controlbox').registerpanel;
spyOn(registerview, 'fetchRegistrationForm').and.callThrough();
spyOn(registerview, 'onProviderChosen').and.callThrough(); spyOn(registerview, 'onProviderChosen').and.callThrough();
spyOn(registerview, 'getRegistrationFields').and.callThrough(); spyOn(registerview, 'getRegistrationFields').and.callThrough();
spyOn(registerview, 'onRegistrationFields').and.callThrough(); spyOn(registerview, 'onRegistrationFields').and.callThrough();
@ -115,7 +116,7 @@ describe("The Registration Panel", function () {
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();
await u.waitUntil(() => _converse.connection.connect.calls.count()); await u.waitUntil(() => registerview.fetchRegistrationForm.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",
@ -294,7 +295,6 @@ describe("The Registration Panel", function () {
mock.initConverse( mock.initConverse(
['chatBoxesInitialized'], ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
view_mode: 'fullscreen',
discover_connection_methods: false, discover_connection_methods: false,
allow_registration: true }, allow_registration: true },
async function (done, _converse) { async function (done, _converse) {

View File

@ -1,4 +1,4 @@
/*global mock */ /*global mock, converse */
const $iq = converse.env.$iq; const $iq = converse.env.$iq;
const $msg = converse.env.$msg; const $msg = converse.env.$msg;
@ -23,8 +23,7 @@ describe("XEP-0198 Stream Management", function () {
await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
let stanza = await u.waitUntil(() => let stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable'), 1000).pop());
sent_stanzas.filter(s => (s.tagName === 'enable')).pop());
expect(_converse.session.get('smacks_enabled')).toBe(false); expect(_converse.session.get('smacks_enabled')).toBe(false);
expect(Strophe.serialize(stanza)).toEqual('<enable resume="true" xmlns="urn:xmpp:sm:3"/>'); expect(Strophe.serialize(stanza)).toEqual('<enable resume="true" xmlns="urn:xmpp:sm:3"/>');
@ -33,7 +32,7 @@ describe("XEP-0198 Stream Management", function () {
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
expect(_converse.session.get('smacks_enabled')).toBe(true); expect(_converse.session.get('smacks_enabled')).toBe(true);
await u.waitUntil(() => view.renderControlBoxPane.calls.count()); await u.waitUntil(() => view.renderControlBoxPane.calls?.count());
let IQ_stanzas = _converse.connection.IQ_stanzas; let IQ_stanzas = _converse.connection.IQ_stanzas;
await u.waitUntil(() => IQ_stanzas.length === 4); await u.waitUntil(() => IQ_stanzas.length === 4);
@ -105,7 +104,7 @@ describe("XEP-0198 Stream Management", function () {
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
IQ_stanzas = _converse.connection.IQ_stanzas; IQ_stanzas = _converse.connection.IQ_stanzas;
await _converse.api.connection.reconnect(); await _converse.api.connection.reconnect();
stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop(), 1000);
expect(Strophe.serialize(stanza)).toEqual('<resume h="2" previd="some-long-sm-id" xmlns="urn:xmpp:sm:3"/>'); 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"/>`); result = u.toStanza(`<resumed xmlns="urn:xmpp:sm:3" h="another-sequence-number" previd="some-long-sm-id"/>`);

View File

@ -310,7 +310,9 @@ converse.plugins.add('converse-register', {
'_registering': true '_registering': true
}); });
await _converse.initConnection(this.domain); await _converse.initConnection(this.domain);
_converse.connection.connect(this.domain, "", status => this.onConnectStatusChanged(status)); // When testing, the test tears down before the async function
// above finishes. So we use optional chaining here
_converse.connection?.connect(this.domain, "", status => this.onConnectStatusChanged(status));
return false; return false;
}, },

407
src/headless/connection.js Normal file
View File

@ -0,0 +1,407 @@
import log from "./log";
import sizzle from 'sizzle';
import u from '@converse/headless/utils/core';
import { Strophe } from 'strophe.js/src/core';
import { __ } from './i18n';
import { _converse, api, clearSession, tearDown } from "./converse-core";
import { isElement, noop } from 'lodash';
const BOSH_WAIT = 59;
/**
* The Connection class manages the connection to the XMPP server. It's
* agnostic concerning the underlying protocol (i.e. websocket, long-polling
* via BOSH or websocket inside a shared worker).
*/
export class Connection extends Strophe.Connection {
static generateResource () {
return `/converse.js-${Math.floor(Math.random()*139749528).toString()}`;
}
async bind () {
/**
* Synchronous event triggered before we send an IQ to bind the user's
* JID resource for this session.
* @event _converse#beforeResourceBinding
*/
await api.trigger('beforeResourceBinding', {'synchronous': true});
super.bind();
}
async onDomainDiscovered (response) {
const text = await response.text();
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 log.warn("Could not discover XEP-0156 connection methods");
}
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'));
if (bosh_methods.length === 0 && ws_methods.length === 0) {
log.warn("Neither BOSH nor WebSocket connection methods have been specified with XEP-0156.");
} else {
// TODO: support multiple endpoints
api.settings.set("websocket_url", ws_methods.pop());
api.settings.set('bosh_service_url', bosh_methods.pop());
this.service = api.settings.get("websocket_url") || api.settings.get('bosh_service_url');
}
}
/**
* Adds support for XEP-0156 by quering the XMPP server for alternate
* connection methods. This allows users to use the websocket or BOSH
* connection of their own XMPP server instead of a proxy provided by the
* host of Converse.js.
* @method Connnection.discoverConnectionMethods
*/
async discoverConnectionMethods (domain) {
// Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods.
const options = {
'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) {
log.error(`Failed to discover alternative connection methods at ${url}`);
log.error(e);
return;
}
if (response.status >= 200 && response.status < 400) {
await this.onDomainDiscovered(response);
} else {
log.warn("Could not discover XEP-0156 connection methods");
}
}
/**
* Establish a new XMPP session by logging in with the supplied JID and
* password.
* @method Connnection.connect
* @param { String } jid
* @param { String } password
* @param { Funtion } callback
*/
async connect (jid, password, callback) {
if (api.settings.get("discover_connection_methods")) {
const domain = Strophe.getDomainFromJid(jid);
await this.discoverConnectionMethods(domain);
}
super.connect(jid, password, callback || this.onConnectStatusChanged, BOSH_WAIT);
}
async reconnect () {
log.debug('RECONNECTING: the connection has dropped, attempting to reconnect.');
this.setConnectionStatus(
Strophe.Status.RECONNECTING,
__('The connection has dropped, attempting to reconnect.')
);
/**
* Triggered when the connection has dropped, but Converse will attempt
* to reconnect again.
*
* @event _converse#will-reconnect
*/
api.trigger('will-reconnect');
this.reconnecting = true;
await tearDown();
return api.user.login();
}
/**
* Called as soon as a new connection has been established, either
* by logging in or by attaching to an existing BOSH session.
* @method Connection.onConnected
* @param { Boolean } reconnecting - Whether Converse.js reconnected from an earlier dropped session.
*/
async onConnected (reconnecting) {
delete this.reconnecting;
this.flush(); // Solves problem of returned PubSub BOSH response not received by browser
await _converse.setUserJID(this.jid);
/**
* Synchronous event triggered after we've sent an IQ to bind the
* user's JID resource for this session.
* @event _converse#afterResourceBinding
*/
await api.trigger('afterResourceBinding', reconnecting, {'synchronous': true});
if (reconnecting) {
/**
* After the connection has dropped and converse.js has reconnected.
* Any Strophe stanza handlers (as registered via `converse.listen.stanza`) will
* have to be registered anew.
* @event _converse#reconnected
* @example _converse.api.listen.on('reconnected', () => { ... });
*/
api.trigger('reconnected');
} else {
/**
* Triggered once converse.js has been initialized.
* See also {@link _converse#event:pluginsInitialized}.
* @event _converse#initialized
*/
api.trigger('initialized');
/**
* Triggered after the connection has been established and Converse
* has got all its ducks in a row.
* @event _converse#initialized
*/
api.trigger('connected');
}
}
/**
* Used to keep track of why we got disconnected, so that we can
* decide on what the next appropriate action is (in onDisconnected)
* @method Connection.setDisconnectionCause
* @param { Number } cause - The status number as received from Strophe.
* @param { String } [reason] - An optional user-facing message as to why
* there was a disconnection.
* @param { Boolean } [override] - An optional flag to replace any previous
* disconnection cause and reason.
*/
setDisconnectionCause (cause, reason, override) {
if (cause === undefined) {
delete this.disconnection_cause;
delete this.disconnection_reason;
} else if (this.disconnection_cause === undefined || override) {
this.disconnection_cause = cause;
this.disconnection_reason = reason;
}
}
setConnectionStatus (status, message) {
this.status = status;
_converse.connfeedback.set({'connection_status': status, message });
}
async finishDisconnection () {
// Properly tear down the session so that it's possible to manually connect again.
log.debug('DISCONNECTED');
delete this.reconnecting;
this.reset();
tearDown();
await clearSession();
delete _converse.connection;
/**
* Triggered after converse.js has disconnected from the XMPP server.
* @event _converse#disconnected
* @memberOf _converse
* @example _converse.api.listen.on('disconnected', () => { ... });
*/
api.trigger('disconnected');
}
/**
* Gets called once strophe's status reaches Strophe.Status.DISCONNECTED.
* Will either start a teardown process for converse.js or attempt
* to reconnect.
* @method onDisconnected
*/
onDisconnected () {
if (api.settings.get("auto_reconnect")) {
const reason = this.disconnection_reason;
if (this.disconnection_cause === Strophe.Status.AUTHFAIL) {
if (api.settings.get("credentials_url") || api.settings.get("authentication") === _converse.ANONYMOUS) {
// If `credentials_url` is set, we reconnect, because we might
// be receiving expirable tokens from the credentials_url.
//
// If `authentication` is anonymous, we reconnect because we
// might have tried to attach with stale BOSH session tokens
// or with a cached JID and password
return api.connection.reconnect();
} else {
return this.finishDisconnection();
}
} else if (
this.disconnection_cause === _converse.LOGOUT ||
reason === Strophe.ErrorCondition.NO_AUTH_MECH ||
reason === "host-unknown" ||
reason === "remote-connection-failed"
) {
return this.finishDisconnection();
}
api.connection.reconnect();
} else {
return this.finishDisconnection();
}
}
/**
* Callback method called by Strophe as the Connection goes
* through various states while establishing or tearing down a
* connection.
* @param { Number } status
* @param { String } message
*/
onConnectStatusChanged (status, message) {
log.debug(`Status changed to: ${_converse.CONNECTION_STATUS[status]}`);
if (status === Strophe.Status.ATTACHFAIL) {
this.setConnectionStatus(status);
this.worker_attach_promise?.resolve(false);
} else if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
if (this.worker_attach_promise?.isResolved && this.status === Strophe.Status.ATTACHED) {
// A different tab must have attached, so nothing to do for us here.
return;
}
this.setConnectionStatus(status);
this.worker_attach_promise?.resolve(true);
// By default we always want to send out an initial presence stanza.
_converse.send_initial_presence = true;
this.setDisconnectionCause();
if (this.reconnecting) {
log.debug(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
this.onConnected(true);
} else {
log.debug(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached');
if (this.restored) {
// No need to send an initial presence stanza when
// we're restoring an existing session.
_converse.send_initial_presence = false;
}
this.onConnected();
}
} else if (status === Strophe.Status.DISCONNECTED) {
this.setDisconnectionCause(status, message);
this.onDisconnected();
} else if (status === Strophe.Status.BINDREQUIRED) {
this.bind();
} else if (status === Strophe.Status.ERROR) {
this.setConnectionStatus(
status,
__('An error occurred while connecting to the chat server.')
);
} else if (status === Strophe.Status.CONNECTING) {
this.setConnectionStatus(status);
} else if (status === Strophe.Status.AUTHENTICATING) {
this.setConnectionStatus(status);
} else if (status === Strophe.Status.AUTHFAIL) {
if (!message) {
message = __('Your XMPP address and/or password is incorrect. Please try again.');
}
this.setConnectionStatus(status, message);
this.setDisconnectionCause(status, message, true);
this.onDisconnected();
} else if (status === Strophe.Status.CONNFAIL) {
let feedback = message;
if (message === "host-unknown" || message == "remote-connection-failed") {
feedback = __("Sorry, we could not connect to the XMPP host with domain: %1$s",
`\"${Strophe.getDomainFromJid(this.jid)}\"`);
} else if (message !== undefined && message === Strophe?.ErrorCondition?.NO_AUTH_MECH) {
feedback = __("The XMPP server did not offer a supported authentication mechanism");
}
this.setConnectionStatus(status, feedback);
this.setDisconnectionCause(status, message);
} else if (status === Strophe.Status.DISCONNECTING) {
this.setDisconnectionCause(status, message);
}
}
isType (type) {
if (type.toLowerCase() === 'websocket') {
return this._proto instanceof Strophe.Websocket;
} else if (type.toLowerCase() === 'bosh') {
return Strophe.BOSH && this._proto instanceof Strophe.Bosh;
}
}
hasResumed () {
if (api.settings.get("connection_options")?.worker || this.isType('bosh')) {
return _converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED;
} else {
// Not binding means that the session was resumed.
return !this.do_bind;
}
}
restoreWorkerSession () {
this.attach(this.onConnectStatusChanged);
this.worker_attach_promise = u.getResolveablePromise();
return this.worker_attach_promise;
}
}
/**
* The MockConnection class is used during testing, to mock an XMPP connection.
* @class
*/
export class MockConnection extends Connection {
constructor (service, options) {
super(service, options);
this.sent_stanzas = [];
this.IQ_stanzas = [];
this.IQ_ids = [];
this.features = Strophe.xmlHtmlNode(
'<stream:features xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">'+
'<ver xmlns="urn:xmpp:features:rosterver"/>'+
'<csi xmlns="urn:xmpp:csi:0"/>'+
'<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">'+
'<required/>'+
'</bind>'+
`<sm xmlns='urn:xmpp:sm:3'/>`+
'<session xmlns="urn:ietf:params:xml:ns:xmpp-session">'+
'<optional/>'+
'</session>'+
'</stream:features>').firstChild;
this._proto._processRequest = noop;
this._proto._disconnect = () => this._onDisconnectTimeout();
this._proto._onDisconnectTimeout = noop;
this._proto._connect = () => {
this.connected = true;
this.mock = true;
this.jid = 'romeo@montague.lit/orchard';
this._changeConnectStatus(Strophe.Status.BINDREQUIRED);
}
}
_processRequest () { // eslint-disable-line class-methods-use-this
// Don't attempt to send out stanzas
}
sendIQ (iq, callback, errback) {
if (!isElement(iq)) {
iq = iq.nodeTree;
}
this.IQ_stanzas.push(iq);
const id = super.sendIQ(iq, callback, errback);
this.IQ_ids.push(id);
return id;
}
send (stanza) {
if (isElement(stanza)) {
this.sent_stanzas.push(stanza);
} else {
this.sent_stanzas.push(stanza.nodeTree);
}
return super.send(stanza);
}
async bind () {
await api.trigger('beforeResourceBinding', {'synchronous': true});
this.authenticated = true;
if (!_converse.no_connection_on_bind) {
this._changeConnectStatus(Strophe.Status.CONNECTED);
}
}
}

View File

@ -5,7 +5,8 @@
*/ */
import './polyfill'; import './polyfill';
import 'strophe.js/src/websocket'; import 'strophe.js/src/websocket';
import * as strophe from 'strophe.js/src/core'; import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
import { Connection, MockConnection } from '@converse/headless/connection.js';
import Storage from '@converse/skeletor/src/storage.js'; import Storage from '@converse/skeletor/src/storage.js';
import _ from './lodash.noconflict'; import _ from './lodash.noconflict';
import advancedFormat from 'dayjs/plugin/advancedFormat'; import advancedFormat from 'dayjs/plugin/advancedFormat';
@ -22,11 +23,6 @@ import { Router } from '@converse/skeletor/src/router.js';
import { __, i18n } from './i18n'; import { __, i18n } from './i18n';
import { assignIn, debounce, invoke, isFunction, isObject, isString, pick } from 'lodash-es'; import { assignIn, debounce, invoke, isFunction, isObject, isString, pick } from 'lodash-es';
const Strophe = strophe.default.Strophe;
const $build = strophe.default.$build;
const $iq = strophe.default.$iq;
const $msg = strophe.default.$msg;
const $pres = strophe.default.$pres;
dayjs.extend(advancedFormat); dayjs.extend(advancedFormat);
@ -82,8 +78,6 @@ class IllegalMessage extends Error {}
// Setting wait to 59 instead of 60 to avoid timing conflicts with the // Setting wait to 59 instead of 60 to avoid timing conflicts with the
// webserver, which is often also set to 60 and might therefore sometimes // webserver, which is often also set to 60 and might therefore sometimes
// return a 504 error page instead of passing through to the BOSH proxy. // return a 504 error page instead of passing through to the BOSH proxy.
const BOSH_WAIT = 59;
const PROMISES = [ const PROMISES = [
'afterResourceBinding', 'afterResourceBinding',
'connectionInitialized', 'connectionInitialized',
@ -233,7 +227,9 @@ export const _converse = {
TimeoutError: TimeoutError, TimeoutError: TimeoutError,
IllegalMessage: IllegalMessage, IllegalMessage: IllegalMessage,
isTestEnv: () => (Strophe.Connection.name === 'MockConnection'), isTestEnv: () => {
return initialization_settings.bosh_service_url === 'montague.lit/http-bind';
},
/** /**
* Translate the given string based on the current locale. * Translate the given string based on the current locale.
@ -296,7 +292,7 @@ function initUserSettings () {
if (!user_settings?.fetched) { if (!user_settings?.fetched) {
const id = `converse.user-settings.${_converse.bare_jid}`; const id = `converse.user-settings.${_converse.bare_jid}`;
user_settings = new Model({id}); user_settings = new Model({id});
user_settings.browserStorage = _converse.createStore(id); user_settings.browserStorage = createStore(id);
user_settings.fetched = user_settings.fetch({'promise': true}); user_settings.fetched = user_settings.fetch({'promise': true});
} }
return user_settings.fetched; return user_settings.fetched;
@ -394,9 +390,9 @@ export const api = _converse.api = {
} }
if (_converse.connection.reconnecting) { if (_converse.connection.reconnecting) {
debouncedReconnect(); _converse.connection.debouncedReconnect();
} else { } else {
return reconnect(); return _converse.connection.reconnect();
} }
}, },
@ -407,11 +403,7 @@ export const api = _converse.api = {
* @returns {boolean} * @returns {boolean}
*/ */
isType (type) { isType (type) {
if (type.toLowerCase() === 'websocket') { return _converse.connection.isType(type);
return _converse.connection._proto instanceof Strophe.Websocket;
} else if (type.toLowerCase() === 'bosh') {
return Strophe.BOSH && _converse.connection._proto instanceof Strophe.Bosh;
}
} }
}, },
@ -508,8 +500,15 @@ export const api = _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 (jid || _converse.jid) { jid = jid || _converse.jid;
jid = await _converse.setUserJID(jid || _converse.jid); if (!_converse.connection?.jid || (jid && !u.isSameDomain(_converse.connection.jid, jid))) {
await _converse.initConnection();
}
if (api.settings.get("connection_options")?.worker && (await _converse.connection.restoreWorkerSession())) {
return;
}
if (jid) {
jid = await _converse.setUserJID(jid);
} }
// See whether there is a BOSH session to re-attach to // See whether there is a BOSH session to re-attach to
@ -521,7 +520,6 @@ export const api = _converse.api = {
return _converse.startNewPreboundBOSHSession(); return _converse.startNewPreboundBOSHSession();
} }
} }
password = password || api.settings.get("password"); password = password || api.settings.get("password");
const credentials = (jid && password) ? { jid, password } : null; const credentials = (jid && password) ? { jid, password } : null;
attemptNonPreboundSession(credentials, automatic); attemptNonPreboundSession(credentials, automatic);
@ -546,7 +544,7 @@ export const api = _converse.api = {
promise.resolve(); promise.resolve();
} }
_converse.setDisconnectionCause(_converse.LOGOUT, undefined, true); _converse.connection.setDisconnectionCause(_converse.LOGOUT, undefined, true);
if (_converse.connection !== undefined) { if (_converse.connection !== undefined) {
api.listen.once('disconnected', () => complete()); api.listen.once('disconnected', () => complete());
_converse.connection.disconnect(); _converse.connection.disconnect();
@ -933,16 +931,6 @@ function replacePromise (name) {
} }
} }
_converse.haveResumed = function () {
if (_converse.api.connection.isType('bosh')) {
return _converse.connfeedback.get('connection_status') === Strophe.Status.ATTACHED;
} else {
// XXX: Not binding means that the session was resumed.
// This seems very fragile. Perhaps a better way is possible.
return !_converse.connection.do_bind;
}
}
_converse.isUniView = function () { _converse.isUniView = function () {
/* We distinguish between UniView and MultiView instances. /* We distinguish between UniView and MultiView instances.
* *
@ -984,11 +972,13 @@ function initPersistentStorage () {
} }
_converse.createStore = function (id, storage) { function createStore (id, storage) {
const s = _converse.storage[storage ? storage : _converse.config.get('storage')]; const s = _converse.storage[storage ? storage : _converse.config.get('storage')];
return new Storage(id, s); return new Storage(id, s);
} }
_converse.createStore = createStore;
function initPlugins () { function initPlugins () {
// If initialize gets called a second time (e.g. during tests), then we // If initialize gets called a second time (e.g. during tests), then we
@ -1046,7 +1036,7 @@ function initClientConfig () {
'trusted': _converse.api.settings.get("trusted") && true || false, 'trusted': _converse.api.settings.get("trusted") && true || false,
'storage': _converse.api.settings.get("trusted") ? 'persistent' : 'session' 'storage': _converse.api.settings.get("trusted") ? 'persistent' : 'session'
}); });
_converse.config.browserStorage = _converse.createStore(id, "session"); _converse.config.browserStorage = createStore(id, "session");
_converse.config.fetch(); _converse.config.fetch();
/** /**
* Triggered once the XMPP-client configuration has been initialized. * Triggered once the XMPP-client configuration has been initialized.
@ -1061,7 +1051,7 @@ function initClientConfig () {
} }
async function tearDown () { export async function tearDown () {
await _converse.api.trigger('beforeTearDown', {'synchronous': true}); await _converse.api.trigger('beforeTearDown', {'synchronous': true});
window.removeEventListener('click', _converse.onUserActivity); window.removeEventListener('click', _converse.onUserActivity);
window.removeEventListener('focus', _converse.onUserActivity); window.removeEventListener('focus', _converse.onUserActivity);
@ -1075,7 +1065,8 @@ async function tearDown () {
async function attemptNonPreboundSession (credentials, automatic) { async function attemptNonPreboundSession (credentials, automatic) {
if (_converse.api.settings.get("authentication") === _converse.LOGIN) { const { api } = _converse;
if (api.settings.get("authentication") === _converse.LOGIN) {
// XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and
// ``authentication`` is set to ``login``, then Converse will try to log the user in, // ``authentication`` is set to ``login``, then Converse will try to log the user in,
// since we don't have a way to distinguish between wether we're // since we don't have a way to distinguish between wether we're
@ -1112,12 +1103,7 @@ function connect (credentials) {
if (!_converse.connection.reconnecting) { if (!_converse.connection.reconnecting) {
_converse.connection.reset(); _converse.connection.reset();
} }
_converse.connection.connect( _converse.connection.connect(_converse.jid.toLowerCase());
_converse.jid.toLowerCase(),
null,
_converse.onConnectStatusChanged,
BOSH_WAIT
);
} else if (_converse.api.settings.get("authentication") === _converse.LOGIN) { } else if (_converse.api.settings.get("authentication") === _converse.LOGIN) {
const password = credentials ? credentials.password : (_converse.connection?.pass || _converse.api.settings.get("password")); const password = credentials ? credentials.password : (_converse.connection?.pass || _converse.api.settings.get("password"));
if (!password) { if (!password) {
@ -1125,51 +1111,25 @@ function connect (credentials) {
throw new Error("autoLogin: If you use auto_login and "+ throw new Error("autoLogin: If you use auto_login and "+
"authentication='login' then you also need to provide a password."); "authentication='login' then you also need to provide a password.");
} }
_converse.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true); _converse.connection.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true);
_converse.api.connection.disconnect(); _converse.api.connection.disconnect();
return; return;
} }
if (!_converse.connection.reconnecting) { if (!_converse.connection.reconnecting) {
_converse.connection.reset(); _converse.connection.reset();
} }
_converse.connection.connect(_converse.jid, password, _converse.onConnectStatusChanged, BOSH_WAIT); _converse.connection.connect(_converse.jid, password);
} }
} }
async function reconnect () {
log.debug('RECONNECTING: the connection has dropped, attempting to reconnect.');
_converse.setConnectionStatus(
Strophe.Status.RECONNECTING,
__('The connection has dropped, attempting to reconnect.')
);
/**
* Triggered when the connection has dropped, but Converse will attempt
* to reconnect again.
*
* @event _converse#will-reconnect
*/
_converse.api.trigger('will-reconnect');
_converse.connection.reconnecting = true;
await tearDown();
return _converse.api.user.login();
}
const debouncedReconnect = debounce(reconnect, 2000);
_converse.shouldClearCache = () => (!_converse.config.get('trusted') || _converse.isTestEnv()); _converse.shouldClearCache = () => (!_converse.config.get('trusted') || _converse.isTestEnv());
function clearSession () { export function clearSession () {
if (_converse.session !== undefined) { _converse.session?.destroy();
_converse.session.destroy(); delete _converse.session;
delete _converse.session; _converse.shouldClearCache() && _converse.api.user.settings.clear();
}
if (_converse.shouldClearCache()) {
_converse.api.user.settings.clear();
}
/** /**
* Synchronouse event triggered once the user session has been cleared, * Synchronouse event triggered once the user session has been cleared,
* for example when the user has logged out or when Converse has * for example when the user has logged out or when Converse has
@ -1180,77 +1140,31 @@ function clearSession () {
} }
async function onDomainDiscovered (response) { _converse.initConnection = function () {
const text = await response.text(); const api = _converse.api;
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 log.warn("Could not discover XEP-0156 connection methods");
}
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.api.settings.set("websocket_url", ws_methods.pop());
_converse.api.settings.set('bosh_service_url', bosh_methods.pop());
if (bosh_methods.length === 0 && ws_methods.length === 0) {
log.warn(
"onDomainDiscovered: neither BOSH nor WebSocket connection methods have been specified with XEP-0156."
);
}
}
if (! api.settings.get('bosh_service_url')) {
async function discoverConnectionMethods (domain) { if (api.settings.get("authentication") === _converse.PREBIND) {
// Use XEP-0156 to check whether this host advertises websocket or BOSH connection methods.
const options = {
'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) {
log.error(`Failed to discover alternative connection methods at ${url}`);
log.error(e);
return;
}
if (response.status >= 200 && response.status < 400) {
await onDomainDiscovered(response);
} else {
log.warn("Could not discover XEP-0156 connection methods");
}
}
_converse.initConnection = async function (domain) {
if (_converse.api.settings.get("discover_connection_methods")) {
await discoverConnectionMethods(domain);
}
if (! _converse.api.settings.get('bosh_service_url')) {
if (_converse.api.settings.get("authentication") === _converse.PREBIND) {
throw new Error("authentication is set to 'prebind' but we don't have a BOSH connection"); throw new Error("authentication is set to 'prebind' but we don't have a BOSH connection");
} }
if (! _converse.api.settings.get("websocket_url")) { if (! api.settings.get("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.api.settings.get("websocket_url")) { const XMPPConnection = _converse.isTestEnv() ? MockConnection : Connection;
_converse.connection = new Strophe.Connection( if (('WebSocket' in window || 'MozWebSocket' in window) && api.settings.get("websocket_url")) {
_converse.api.settings.get("websocket_url"), _converse.connection = new XMPPConnection(
Object.assign(_converse.default_connection_options, _converse.api.settings.get("connection_options")) api.settings.get("websocket_url"),
Object.assign(_converse.default_connection_options, api.settings.get("connection_options"))
); );
} else if (_converse.api.settings.get('bosh_service_url')) { } else if (api.settings.get('bosh_service_url')) {
_converse.connection = new Strophe.Connection( _converse.connection = new XMPPConnection(
_converse.api.settings.get('bosh_service_url'), api.settings.get('bosh_service_url'),
Object.assign( Object.assign(
_converse.default_connection_options, _converse.default_connection_options,
_converse.api.settings.get("connection_options"), api.settings.get("connection_options"),
{'keepalive': _converse.api.settings.get("keepalive")} {'keepalive': api.settings.get("keepalive")}
) )
); );
} else { } else {
@ -1259,23 +1173,28 @@ _converse.initConnection = async function (domain) {
} }
setUpXMLLogging(); setUpXMLLogging();
/** /**
* Triggered once the `Strophe.Connection` constructor has been initialized, which * Triggered once the `Connection` constructor has been initialized, which
* will be responsible for managing the connection to the XMPP server. * will be responsible for managing the connection to the XMPP server.
* *
* @event _converse#connectionInitialized * @event _converse#connectionInitialized
*/ */
_converse.api.trigger('connectionInitialized'); api.trigger('connectionInitialized');
} }
async function initSession (jid) { async function initSession (jid) {
const is_shared_session = api.settings.get('connection_options').worker;
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?.get('id') !== id) {
_converse.session = new Model({id}); _converse.session = new Model({id});
_converse.session.browserStorage = _converse.createStore(id, "session"); _converse.session.browserStorage = createStore(id, is_shared_session ? "persistent" : "session");
await new Promise(r => _converse.session.fetch({'success': r, 'error': r})); await new Promise(r => _converse.session.fetch({'success': r, 'error': r}));
if (_converse.session.get('active')) {
if (!is_shared_session && _converse.session.get('active')) {
// If the `active` flag is set, it means this tab was cloned from
// another (e.g. via middle-click), and its session data was copied over.
_converse.session.clear(); _converse.session.clear();
_converse.session.save({id}); _converse.session.save({id});
} }
@ -1297,7 +1216,7 @@ async function initSession (jid) {
function saveJIDtoSession (jid) { function saveJIDtoSession (jid) {
jid = _converse.session.get('jid') || jid; jid = _converse.session.get('jid') || jid;
if (_converse.api.settings.get("authentication") !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) { if (_converse.api.settings.get("authentication") !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
jid = jid.toLowerCase() + _converse.generateResource(); jid = jid.toLowerCase() + Connection.generateResource();
} }
_converse.jid = jid; _converse.jid = jid;
_converse.bare_jid = Strophe.getBareJidFromJid(jid); _converse.bare_jid = Strophe.getBareJidFromJid(jid);
@ -1308,6 +1227,9 @@ function saveJIDtoSession (jid) {
'bare_jid': _converse.bare_jid, 'bare_jid': _converse.bare_jid,
'resource': _converse.resource, 'resource': _converse.resource,
'domain': _converse.domain, 'domain': _converse.domain,
// We use the `active` flag to determine whether we should use the values from sessionStorage.
// When "cloning" a tab (e.g. via middle-click), the `active` flag will be set and we'll create
// a new empty user session, otherwise it'll be false and we can re-use the user session.
'active': true 'active': true
}); });
// Set JID on the connection object so that when we call `connection.bind` // Set JID on the connection object so that when we call `connection.bind`
@ -1330,10 +1252,6 @@ function saveJIDtoSession (jid) {
* @params { String } jid * @params { String } jid
*/ */
_converse.setUserJID = async function (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); await initSession(jid);
/** /**
* Triggered whenever the user's JID has been updated * Triggered whenever the user's JID has been updated
@ -1405,95 +1323,6 @@ function cleanup () {
_converse.off(); _converse.off();
} }
_converse.generateResource = () => `/converse.js-${Math.floor(Math.random()*139749528).toString()}`;
/**
* Callback method called by Strophe as the Strophe.Connection goes
* through various states while establishing or tearing down a
* connection.
* @method _converse#onConnectStatusChanged
* @private
* @memberOf _converse
*/
_converse.onConnectStatusChanged = function (status, message) {
log.debug(`Status changed to: ${_converse.CONNECTION_STATUS[status]}`);
if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
_converse.setConnectionStatus(status);
// By default we always want to send out an initial presence stanza.
_converse.send_initial_presence = true;
_converse.setDisconnectionCause();
if (_converse.connection.reconnecting) {
log.debug(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
onConnected(true);
} else {
log.debug(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached');
if (_converse.connection.restored) {
// No need to send an initial presence stanza when
// we're restoring an existing session.
_converse.send_initial_presence = false;
}
onConnected();
}
} else if (status === Strophe.Status.DISCONNECTED) {
_converse.setDisconnectionCause(status, message);
_converse.onDisconnected();
} else if (status === Strophe.Status.BINDREQUIRED) {
_converse.bindResource();
} else if (status === Strophe.Status.ERROR) {
_converse.setConnectionStatus(
status,
__('An error occurred while connecting to the chat server.')
);
} else if (status === Strophe.Status.CONNECTING) {
_converse.setConnectionStatus(status);
} else if (status === Strophe.Status.AUTHENTICATING) {
_converse.setConnectionStatus(status);
} else if (status === Strophe.Status.AUTHFAIL) {
if (!message) {
message = __('Your XMPP address and/or password is incorrect. Please try again.');
}
_converse.setConnectionStatus(status, message);
_converse.setDisconnectionCause(status, message, true);
_converse.onDisconnected();
} else if (status === Strophe.Status.CONNFAIL) {
let feedback = message;
if (message === "host-unknown" || message == "remote-connection-failed") {
feedback = __("Sorry, we could not connect to the XMPP host with domain: %1$s",
`\"${Strophe.getDomainFromJid(_converse.connection.jid)}\"`);
} else if (message !== undefined && message === Strophe?.ErrorCondition?.NO_AUTH_MECH) {
feedback = __("The XMPP server did not offer a supported authentication mechanism");
}
_converse.setConnectionStatus(status, feedback);
_converse.setDisconnectionCause(status, message);
} else if (status === Strophe.Status.DISCONNECTING) {
_converse.setDisconnectionCause(status, message);
}
};
_converse.setConnectionStatus = function (connection_status, message) {
_converse.connfeedback.set({
'connection_status': connection_status,
'message': message
});
};
/**
* Used to keep track of why we got disconnected, so that we can
* decide on what the next appropriate action is (in onDisconnected)
*/
_converse.setDisconnectionCause = function (cause, reason, override) {
if (cause === undefined) {
delete _converse.disconnection_cause;
delete _converse.disconnection_reason;
} else if (_converse.disconnection_cause === undefined || override) {
_converse.disconnection_cause = cause;
_converse.disconnection_reason = reason;
}
};
function enableCarbons () { function enableCarbons () {
/* Ask the XMPP server to enable Message Carbons /* Ask the XMPP server to enable Message Carbons
@ -1519,65 +1348,7 @@ function enableCarbons () {
_converse.connection.send(carbons_iq); _converse.connection.send(carbons_iq);
} }
api.listen.on('afterResourceBinding', () => enableCarbons());
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.
*/
delete _converse.connection.reconnecting;
_converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser
await _converse.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 api.trigger('afterResourceBinding', reconnecting, {'synchronous': true});
enableCarbons();
if (reconnecting) {
/**
* After the connection has dropped and converse.js has reconnected.
* Any Strophe stanza handlers (as registered via `converse.listen.stanza`) will
* have to be registered anew.
* @event _converse#reconnected
* @example _converse.api.listen.on('reconnected', () => { ... });
*/
api.trigger('reconnected');
} else {
/**
* Triggered once converse.js has been initialized.
* See also {@link _converse#event:pluginsInitialized}.
* @event _converse#initialized
*/
api.trigger('initialized');
/**
* Triggered after the connection has been established and Converse
* has got all its ducks in a row.
* @event _converse#initialized
*/
api.trigger('connected');
}
}
async function finishDisconnection () {
// Properly tear down the session so that it's possible to manually connect again.
log.debug('DISCONNECTED');
delete _converse.connection.reconnecting;
_converse.connection.reset();
tearDown();
await clearSession();
delete _converse.connection;
/**
* Triggered after converse.js has disconnected from the XMPP server.
* @event _converse#disconnected
* @memberOf _converse
* @example _converse.api.listen.on('disconnected', () => { ... });
*/
api.trigger('disconnected');
}
function fetchLoginCredentials (wait=0) { function fetchLoginCredentials (wait=0) {
@ -1659,55 +1430,6 @@ function unregisterGlobalEventHandlers () {
} }
/**
* Gets called once strophe's status reaches Strophe.Status.DISCONNECTED.
* Will either start a teardown process for converse.js or attempt
* to reconnect.
* @method onDisconnected
* @private
* @memberOf _converse
*/
_converse.onDisconnected = function () {
if (api.settings.get("auto_reconnect")) {
const reason = _converse.disconnection_reason;
if (_converse.disconnection_cause === Strophe.Status.AUTHFAIL) {
if (api.settings.get("credentials_url") || api.settings.get("authentication") === _converse.ANONYMOUS) {
// If `credentials_url` is set, we reconnect, because we might
// be receiving expirable tokens from the credentials_url.
//
// If `authentication` is anonymous, we reconnect because we
// might have tried to attach with stale BOSH session tokens
// or with a cached JID and password
return api.connection.reconnect();
} else {
return finishDisconnection();
}
} else if (
_converse.disconnection_cause === _converse.LOGOUT ||
reason === Strophe.ErrorCondition.NO_AUTH_MECH ||
reason === "host-unknown" ||
reason === "remote-connection-failed"
) {
return finishDisconnection();
}
api.connection.reconnect();
} else {
return finishDisconnection();
}
};
_converse.bindResource = async function () {
/**
* Synchronous event triggered before we send an IQ to bind the user's
* JID resource for this session.
* @event _converse#beforeResourceBinding
*/
await api.trigger('beforeResourceBinding', {'synchronous': true});
_converse.connection.bind();
};
_converse.ConnectionFeedback = Model.extend({ _converse.ConnectionFeedback = Model.extend({
defaults: { defaults: {
'connection_status': Strophe.Status.DISCONNECTED, 'connection_status': Strophe.Status.DISCONNECTED,

View File

@ -146,8 +146,8 @@ converse.plugins.add('converse-mam', {
_converse.onMAMError = function (iq) { _converse.onMAMError = function (iq) {
if (iq && iq.querySelectorAll('feature-not-implemented').length) { if (iq?.querySelectorAll('feature-not-implemented').length) {
log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`); log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`);
} else { } else {
log.error(`Error while trying to set archiving preferences for ${iq.getAttribute('from')}.`); log.error(`Error while trying to set archiving preferences for ${iq.getAttribute('from')}.`);
log.error(iq); log.error(iq);

View File

@ -960,7 +960,7 @@ converse.plugins.add('converse-roster', {
// When reconnecting and not resuming a previous session, // When reconnecting and not resuming a previous session,
// we clear all cached presence data, since it might be stale // we clear all cached presence data, since it might be stale
// and we'll receive new presence updates // and we'll receive new presence updates
!_converse.haveResumed() && await clearPresences(); !_converse.connection.hasResumed() && await clearPresences();
} else { } else {
_converse.presences = new _converse.Presences(); _converse.presences = new _converse.Presences();
const id = `converse.presences-${_converse.bare_jid}`; const id = `converse.presences-${_converse.bare_jid}`;

View File

@ -4,40 +4,27 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@converse/skeletor": {
"version": "github:conversejs/skeletor#b260c554f4ce961c29deea4740083e58a489aa9b",
"from": "github:conversejs/skeletor#b260c554f4ce961c29deea4740083e58a489aa9b",
"dev": true,
"requires": {
"lit-html": "^1.2.1",
"lodash-es": "^4.17.14"
}
},
"abab": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
"integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==",
"dev": true
},
"filesize": { "filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==",
"dev": true "dev": true
}, },
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"dependencies": {
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
}
}
}
},
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
},
"immediate": { "immediate": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@ -50,16 +37,6 @@
"integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=", "integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=",
"dev": true "dev": true
}, },
"jsonfile": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz",
"integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^0.1.2"
}
},
"lie": { "lie": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
@ -69,6 +46,12 @@
"immediate": "~3.0.5" "immediate": "~3.0.5"
} }
}, },
"lit-html": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.2.1.tgz",
"integrity": "sha512-GSJHHXMGLZDzTRq59IUfL9FCdAlGfqNp/dEa7k7aBaaWD+JKaCjsAk9KYm2V12ItonVaYx2dprN66Zdm1AuBTQ==",
"dev": true
},
"localforage": { "localforage": {
"version": "1.7.3", "version": "1.7.3",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
@ -81,6 +64,7 @@
"lodash": { "lodash": {
"version": "4.17.19", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true "dev": true
}, },
"lodash-es": { "lodash-es": {
@ -95,32 +79,32 @@
"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==", "integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "^4.17.19" "lodash": "^4.17.11"
} }
}, },
"twemoji": { "strophe.js": {
"version": "12.1.5", "version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-12.1.5.tgz", "from": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"integrity": "sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"fs-extra": "^8.0.1", "abab": "^2.0.3",
"jsonfile": "^5.0.0", "ws": "^7.0.0",
"twemoji-parser": "12.1.3", "xmldom": "^0.1.27"
"universalify": "^0.1.2"
} }
}, },
"twemoji-parser": { "ws": {
"version": "12.1.3", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-12.1.3.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-ND4LZXF4X92/PFrzSgGkq6KPPg8swy/U0yRw1k/+izWRVmq1HYi3khPwV3XIB6FRudgVICAaBhJfW8e8G3HC7Q==", "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
"dev": true "dev": true,
"optional": true
}, },
"universalify": { "xmldom": {
"version": "0.1.2", "version": "0.1.31",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==",
"dev": true "dev": true,
"optional": true
} }
} }
} }

View File

@ -42,6 +42,6 @@
"localforage": "^1.7.3", "localforage": "^1.7.3",
"lodash-es": "^4.17.15", "lodash-es": "^4.17.15",
"pluggable.js": "2.0.1", "pluggable.js": "2.0.1",
"strophe.js": "1.3.6" "strophe.js": "strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f"
} }
} }

View File

@ -1,3 +1,3 @@
import { html } from "lit-html"; import { html } from "lit-html";
export default (o) => html`<span class="spinner fa fa-spinner centered ${o.classes || ''}"/>` export default (o={}) => html`<span class="spinner fa fa-spinner centered ${o.classes || ''}"/>`

View File

@ -89,7 +89,7 @@ module.exports = {
] ]
}, { }, {
test: /\.js$/, test: /\.js$/,
exclude: /(node_modules|spec|mockup)/, include: /src/,
use: { use: {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {

View File

@ -28,6 +28,7 @@
modtools_disable_query: ['moderator', 'participant', 'visitor'], modtools_disable_query: ['moderator', 'participant', 'visitor'],
enable_smacks: true, enable_smacks: true,
i18n: 'en', i18n: 'en',
// connection_options: { 'worker': '/dist/shared-connection-worker.js' },
message_archiving: 'always', message_archiving: 'always',
muc_domain: 'conference.chat.example.org', muc_domain: 'conference.chat.example.org',
muc_respect_autojoin: true, muc_respect_autojoin: true,

View File

@ -20,6 +20,7 @@ module.exports = merge(common, {
new MiniCssExtractPlugin({filename: '../dist/converse.min.css'}), new MiniCssExtractPlugin({filename: '../dist/converse.min.css'}),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [ patterns: [
{from: 'src/headless/node_modules/strophe.js/src/shared-connection-worker.js', to: 'shared-connection-worker.js'},
{from: 'sounds', to: 'sounds'}, {from: 'sounds', to: 'sounds'},
{from: 'images/favicon.ico', to: 'images/favicon.ico'}, {from: 'images/favicon.ico', to: 'images/favicon.ico'},
{from: 'images/custom_emojis', to: 'images/custom_emojis'}, {from: 'images/custom_emojis', to: 'images/custom_emojis'},