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`+