diff --git a/CHANGES.md b/CHANGES.md
index dbb3f7bd8..4fa84690b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -24,6 +24,7 @@
- Use the MUC stanza id when sending XEP-0333 markers
- Add support for rendering unfurls via [mod_ogp](https://modules.prosody.im/mod_ogp.html)
- Add a Description Of A Project (DOAP) file
+- Add ability to deregister nickname when closing a MUC by setting `auto_register_muc_nickname` to `'unregister'`.
### Breaking Changes
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index 65661e8e4..6c1af0f7c 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -435,12 +435,17 @@ auto_register_muc_nickname
--------------------------
* Default: ``false``
+* Allowed values: ``false``, ``true``, ``'unregister'``
-Determines whether Converse should automatically register a user's nickname
-when they enter a groupchat.
+If truthy, Converse will automatically register a user's nickname upon entering
+a groupchat.
See here fore more details: https://xmpp.org/extensions/xep-0045.html#register
+If set to ``'unregister'``, then the user's nickname will be registered
+(because it's a truthy value) and also be unregistered when the user
+permanently leaves the MUC by closing it.
+
auto_subscribe
--------------
diff --git a/karma.conf.js b/karma.conf.js
index 8f3cbbc35..ef30f9a04 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -33,6 +33,7 @@ module.exports = function(config) {
{ pattern: "src/headless/plugins/chat/tests/api.js", type: 'module' },
{ pattern: "src/headless/plugins/disco/tests/disco.js", type: 'module' },
{ pattern: "src/headless/plugins/muc/tests/affiliations.js", type: 'module' },
+ { pattern: "src/headless/plugins/muc/tests/registration.js", type: 'module' },
{ pattern: "src/headless/plugins/ping/tests/ping.js", type: 'module' },
{ pattern: "src/headless/plugins/roster/tests/presence.js", type: 'module' },
{ pattern: "src/headless/plugins/smacks/tests/smacks.js", type: 'module' },
diff --git a/spec/mock.js b/spec/mock.js
index 807a04720..717ab2a99 100644
--- a/spec/mock.js
+++ b/spec/mock.js
@@ -322,7 +322,8 @@ mock.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[
const affs = _converse.muc_fetch_members;
const all_affiliations = Array.isArray(affs) ? affs : (affs ? ['member', 'admin', 'owner'] : []);
await mock.returnMemberLists(_converse, muc_jid, members, all_affiliations);
- return model.messages.fetched;
+ await model.messages.fetched;
+ return model;
};
mock.createContact = async function (_converse, name, ask, requesting, subscription) {
diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js
index 8ad6de9da..f22042e2e 100644
--- a/src/headless/plugins/muc/muc.js
+++ b/src/headless/plugins/muc/muc.js
@@ -845,6 +845,12 @@ const ChatRoomMixin = {
async close (ev) {
await this.leave();
+ if (
+ api.settings.get('auto_register_muc_nickname') === 'unregister' &&
+ (await api.disco.supports(Strophe.NS.MUC_REGISTER, this.get('jid')))
+ ) {
+ this.unregisterNickname();
+ }
this.occupants.clearStore();
if (ev?.name !== 'closeAllChatBoxes' && api.settings.get('muc_clear_messages_on_leave')) {
@@ -1533,7 +1539,7 @@ const ChatRoomMixin = {
async registerNickname () {
// See https://xmpp.org/extensions/xep-0045.html#register
- const __ = _converse.__;
+ const { __ } = _converse;
const nick = this.get('nick');
const jid = this.get('jid');
let iq, err_msg;
@@ -1541,7 +1547,6 @@ const ChatRoomMixin = {
iq = await api.sendIQ(
$iq({
'to': jid,
- 'from': _converse.connection.jid,
'type': 'get'
}).c('query', { 'xmlns': Strophe.NS.MUC_REGISTER })
);
@@ -1562,19 +1567,13 @@ const ChatRoomMixin = {
await api.sendIQ(
$iq({
'to': jid,
- 'from': _converse.connection.jid,
'type': 'set'
- })
- .c('query', { 'xmlns': Strophe.NS.MUC_REGISTER })
+ }).c('query', { 'xmlns': Strophe.NS.MUC_REGISTER })
.c('x', { 'xmlns': Strophe.NS.XFORM, 'type': 'submit' })
- .c('field', { 'var': 'FORM_TYPE' })
- .c('value')
- .t('http://jabber.org/protocol/muc#register')
- .up()
- .up()
- .c('field', { 'var': 'muc#register_roomnick' })
- .c('value')
- .t(nick)
+ .c('field', { 'var': 'FORM_TYPE' })
+ .c('value').t('http://jabber.org/protocol/muc#register').up().up()
+ .c('field', { 'var': 'muc#register_roomnick' })
+ .c('value').t(nick)
);
} catch (e) {
if (sizzle(`service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
@@ -1588,6 +1587,28 @@ const ChatRoomMixin = {
}
},
+ async unregisterNickname () {
+ const jid = this.get('jid');
+ let iq;
+ try {
+ iq = await api.sendIQ(
+ $iq({
+ 'to': jid,
+ 'type': 'set'
+ }).c('query', { 'xmlns': Strophe.NS.MUC_REGISTER })
+ );
+ } catch (e) {
+ log.error(e);
+ return e;
+ }
+ if (sizzle(`query[xmlns="${Strophe.NS.MUC_REGISTER}"] registered`, iq).pop()) {
+ const iq = $iq({ 'to': jid, 'type': 'set' })
+ .c('query', { 'xmlns': Strophe.NS.MUC_REGISTER })
+ .c('remove');
+ return api.sendIQ(iq).catch(e => log.error(e));
+ }
+ },
+
/**
* Given a presence stanza, update the occupant model based on its contents.
* @private
diff --git a/src/headless/plugins/muc/tests/registration.js b/src/headless/plugins/muc/tests/registration.js
new file mode 100644
index 000000000..94bb80671
--- /dev/null
+++ b/src/headless/plugins/muc/tests/registration.js
@@ -0,0 +1,116 @@
+/*global mock, converse */
+
+const { $iq, Strophe, sizzle, u } = converse.env;
+
+describe("Chatrooms", function () {
+
+ describe("The auto_register_muc_nickname option", function () {
+
+ it("allows you to automatically register your nickname when joining a room",
+ mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': true},
+ async function (done, _converse) {
+
+ const muc_jid = 'coven@chat.shakespeare.lit';
+ const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+
+ let stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
+ iq => sizzle(`iq[to="${muc_jid}"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length
+ ).pop());
+
+ expect(Strophe.serialize(stanza))
+ .toBe(``+
+ ``);
+ const result = $iq({
+ 'from': room.get('jid'),
+ 'id': stanza.getAttribute('id'),
+ 'to': _converse.bare_jid,
+ 'type': 'result',
+ }).c('query', {'xmlns': 'jabber:iq:register'})
+ .c('x', {'xmlns': 'jabber:x:data', 'type': 'form'})
+ .c('field', {
+ 'label': 'Desired Nickname',
+ 'type': 'text-single',
+ 'var': 'muc#register_roomnick'
+ }).c('required');
+ _converse.connection._dataRecv(mock.createRequest(result));
+ stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
+ iq => sizzle(`iq[to="${muc_jid}"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length
+ ).pop());
+
+ expect(Strophe.serialize(stanza)).toBe(
+ ``+
+ ``+
+ ``+
+ `http://jabber.org/protocol/muc#register`+
+ `romeo`+
+ ``+
+ ``+
+ ``);
+ done();
+ }));
+
+ it("allows you to automatically deregister your nickname when closing a room",
+ mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': 'unregister'},
+ async function (done, _converse) {
+
+ const muc_jid = 'coven@chat.shakespeare.lit';
+ const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+
+ let stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
+ iq => sizzle(`iq[to="${muc_jid}"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length
+ ).pop());
+ let result = $iq({
+ 'from': room.get('jid'),
+ 'id': stanza.getAttribute('id'),
+ 'to': _converse.bare_jid,
+ 'type': 'result',
+ }).c('query', {'xmlns': 'jabber:iq:register'})
+ .c('x', {'xmlns': 'jabber:x:data', 'type': 'form'})
+ .c('field', {
+ 'label': 'Desired Nickname',
+ 'type': 'text-single',
+ 'var': 'muc#register_roomnick'
+ }).c('required');
+ _converse.connection._dataRecv(mock.createRequest(result));
+ await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
+ iq => sizzle(`iq[to="${muc_jid}"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length
+ ).pop());
+
+ _converse.connection.IQ_stanzas = [];
+ room.close();
+ stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
+ iq => sizzle(`iq[to="${muc_jid}"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length
+ ).pop());
+ _converse.connection.IQ_stanzas = [];
+
+ result = $iq({
+ 'from': room.get('jid'),
+ 'id': stanza.getAttribute('id'),
+ 'to': _converse.bare_jid,
+ 'type': 'result',
+ }).c('query', {'xmlns': 'jabber:iq:register'})
+ .c('registered').up()
+ .c('username').t('romeo');
+ _converse.connection._dataRecv(mock.createRequest(result));
+
+ stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
+ iq => sizzle(`iq[to="${muc_jid}"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length
+ ).pop());
+ expect(Strophe.serialize(stanza)).toBe(
+ ``+
+ ``+
+ ``);
+
+ result = $iq({
+ 'from': room.get('jid'),
+ 'id': stanza.getAttribute('id'),
+ 'to': _converse.bare_jid,
+ 'type': 'result',
+ }).c('query', {'xmlns': 'jabber:iq:register'});
+ _converse.connection._dataRecv(mock.createRequest(result));
+
+ done();
+ }));
+ });
+});
diff --git a/src/plugins/muc-views/tests/muc-registration.js b/src/plugins/muc-views/tests/muc-registration.js
index f985cd43a..a0661e0c8 100644
--- a/src/plugins/muc-views/tests/muc-registration.js
+++ b/src/plugins/muc-views/tests/muc-registration.js
@@ -1,9 +1,6 @@
-/*global mock, converse, _ */
+/*global mock, converse */
-const $iq = converse.env.$iq,
- Strophe = converse.env.Strophe,
- sizzle = converse.env.sizzle,
- u = converse.env.utils;
+const { $iq, Strophe, sizzle, u } = converse.env;
describe("Chatrooms", function () {
@@ -24,12 +21,11 @@ describe("Chatrooms", function () {
preventDefault: function preventDefault () {},
keyCode: 13
});
- let stanza = await u.waitUntil(() => _.filter(
- _converse.connection.IQ_stanzas,
+ let stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => sizzle(`iq[to="${muc_jid}"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length
).pop());
expect(Strophe.serialize(stanza))
- .toBe(``+
``);
const result = $iq({
@@ -45,64 +41,12 @@ describe("Chatrooms", function () {
'var': 'muc#register_roomnick'
}).c('required');
_converse.connection._dataRecv(mock.createRequest(result));
- stanza = await u.waitUntil(() => _.filter(
- _converse.connection.IQ_stanzas,
+ stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => sizzle(`iq[to="${muc_jid}"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length
).pop());
expect(Strophe.serialize(stanza)).toBe(
- ``+
- ``+
- ``+
- `http://jabber.org/protocol/muc#register`+
- `romeo`+
- ``+
- ``+
- ``);
- done();
- }));
-
- });
-
- describe("The auto_register_muc_nickname option", function () {
-
- it("allows you to automatically register your nickname when joining a room",
- mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': true},
- async function (done, _converse) {
-
- const muc_jid = 'coven@chat.shakespeare.lit';
- await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
- const view = _converse.chatboxviews.get(muc_jid);
-
- let stanza = await u.waitUntil(() => _.filter(
- _converse.connection.IQ_stanzas,
- iq => sizzle(`iq[to="coven@chat.shakespeare.lit"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length
- ).pop());
-
- expect(Strophe.serialize(stanza))
- .toBe(``+
- ``);
- const result = $iq({
- 'from': view.model.get('jid'),
- 'id': stanza.getAttribute('id'),
- 'to': _converse.bare_jid,
- 'type': 'result',
- }).c('query', {'type': 'jabber:iq:register'})
- .c('x', {'xmlns': 'jabber:x:data', 'type': 'form'})
- .c('field', {
- 'label': 'Desired Nickname',
- 'type': 'text-single',
- 'var': 'muc#register_roomnick'
- }).c('required');
- _converse.connection._dataRecv(mock.createRequest(result));
- stanza = await u.waitUntil(() => _.filter(
- _converse.connection.IQ_stanzas,
- iq => sizzle(`iq[to="coven@chat.shakespeare.lit"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length
- ).pop());
-
- expect(Strophe.serialize(stanza)).toBe(
- ``+
+ ``+
``+
``+
`http://jabber.org/protocol/muc#register`+