From fa562cabae5de6d54af814bcf3a1b992018ef23b Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 21 Nov 2021 11:05:23 +0100 Subject: [PATCH] Don't fetch member list if not affiliated Fixes #1426 --- CHANGES.md | 1 + karma.conf.js | 1 + src/headless/plugins/muc/constants.js | 6 + src/headless/plugins/muc/muc.js | 3 +- src/headless/plugins/muc/occupants.js | 37 ++- src/plugins/muc-views/tests/member-lists.js | 301 ++++++++++++++++++++ src/plugins/muc-views/tests/muc.js | 291 +------------------ src/shared/tests/mock.js | 27 +- 8 files changed, 347 insertions(+), 320 deletions(-) create mode 100644 src/headless/plugins/muc/constants.js create mode 100644 src/plugins/muc-views/tests/member-lists.js diff --git a/CHANGES.md b/CHANGES.md index 698c04322..633618a19 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - If `auto_register_muc_nickname` is set, make sure to register when the user changes current nick. - #1322: Display occupants’ avatars in the occupants list - #1419: Clicking on avatar should show bigger version +- #1426: Don't fetch member list if not affiliated - #2647: Singleton mode doesn't work - #2704: Send button doesn't work in a multi-user chat - #2725: Send new presence status to all connected MUCs diff --git a/karma.conf.js b/karma.conf.js index e5210d5a0..076687a07 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -79,6 +79,7 @@ module.exports = function(config) { { pattern: "src/plugins/muc-views/tests/mentions.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/mep.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/modtools.js", type: 'module' }, + { pattern: "src/plugins/muc-views/tests/member-lists.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/muc-api.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/muc-mentions.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/muc-messages.js", type: 'module' }, diff --git a/src/headless/plugins/muc/constants.js b/src/headless/plugins/muc/constants.js new file mode 100644 index 000000000..fef4063dd --- /dev/null +++ b/src/headless/plugins/muc/constants.js @@ -0,0 +1,6 @@ +export const MUC_ROLE_WEIGHTS = { + 'moderator': 1, + 'participant': 2, + 'visitor': 3, + 'none': 2 +}; diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index 883c00719..8c96f5a61 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -1396,12 +1396,11 @@ const ChatRoomMixin = { /** * Get the {@link _converse.ChatRoomOccupant} instance which * represents the current user. - * @private * @method _converse.ChatRoom#getOwnOccupant * @returns { _converse.ChatRoomOccupant } */ getOwnOccupant () { - return this.occupants.findWhere({ 'jid': _converse.bare_jid }); + return this.occupants.getOwnOccupant(); }, async setNickname (nick) { diff --git a/src/headless/plugins/muc/occupants.js b/src/headless/plugins/muc/occupants.js index 9b178676a..e0cef559c 100644 --- a/src/headless/plugins/muc/occupants.js +++ b/src/headless/plugins/muc/occupants.js @@ -1,17 +1,11 @@ import ChatRoomOccupant from './occupant.js'; import u from '../../utils/form'; import { Collection } from '@converse/skeletor/src/collection'; +import { MUC_ROLE_WEIGHTS } from './constants.js'; import { Strophe } from 'strophe.js/src/strophe'; import { _converse, api } from '../../core.js'; import { getAffiliationList } from './affiliations/utils.js'; -const MUC_ROLE_WEIGHTS = { - 'moderator': 1, - 'participant': 2, - 'visitor': 3, - 'none': 2 -}; - /** * A list of {@link _converse.ChatRoomOccupant} instances, representing participants in a MUC. @@ -34,12 +28,26 @@ const ChatRoomOccupants = Collection.extend({ } }, + /** + * Get the {@link _converse.ChatRoomOccupant} instance which + * represents the current user. + * @method _converse.ChatRoomOccupants#getOwnOccupant + * @returns { _converse.ChatRoomOccupant } + */ + getOwnOccupant () { + return this.findWhere({ 'jid': _converse.bare_jid }); + }, + getAutoFetchedAffiliationLists () { const affs = api.settings.get('muc_fetch_members'); return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : []; }, async fetchMembers () { + if (!['member', 'admin', 'owner'].includes(this.getOwnOccupant()?.get('affiliation'))) { + // https://xmpp.org/extensions/xep-0045.html#affil-priv + return; + } const affiliations = this.getAutoFetchedAffiliationLists(); if (affiliations.length === 0) { return; @@ -59,26 +67,18 @@ const ChatRoomOccupants = Collection.extend({ !new_jids.includes(m.get('jid')) ); }); - removed_members.forEach(occupant => { if (occupant.get('jid') === _converse.bare_jid) { return; - } - if (occupant.get('show') === 'offline') { + } else if (occupant.get('show') === 'offline') { occupant.destroy(); } else { occupant.save('affiliation', null); } }); new_members.forEach(attrs => { - const occupant = attrs.jid - ? this.findOccupant({ 'jid': attrs.jid }) - : this.findOccupant({ 'nick': attrs.nick }); - if (occupant) { - occupant.save(attrs); - } else { - this.create(attrs); - } + const occupant = this.findOccupant(attrs); + occupant ? occupant.save(attrs) : this.create(attrs); }); /** * Triggered once the member lists for this MUC have been fetched and processed. @@ -101,7 +101,6 @@ const ChatRoomOccupants = Collection.extend({ * If we have a JID, we use that as lookup variable, * otherwise we use the nick. We don't always have both, * but should have at least one or the other. - * @private * @method _converse.ChatRoomOccupants#findOccupant * @param { OccupantData } data */ diff --git a/src/plugins/muc-views/tests/member-lists.js b/src/plugins/muc-views/tests/member-lists.js new file mode 100644 index 000000000..fc0b9d169 --- /dev/null +++ b/src/plugins/muc-views/tests/member-lists.js @@ -0,0 +1,301 @@ +/*global mock, converse */ +const { $iq, Strophe, u } = converse.env; + +describe("A Groupchat", function () { + + describe("upon being entered", function () { + + it("will fetch the member list if muc_fetch_members is true", + mock.initConverse([], {'muc_fetch_members': true}, async function (_converse) { + + const { api } = _converse; + let sent_IQs = _converse.connection.IQ_stanzas; + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + let view = _converse.chatboxviews.get(muc_jid); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(3); + + // Check in reverse order that we requested all three lists + const owner_iq = sent_IQs.pop(); + expect(Strophe.serialize(owner_iq)).toBe( + ``+ + ``+ + ``); + + const admin_iq = sent_IQs.pop(); + expect(Strophe.serialize(admin_iq)).toBe( + ``+ + ``+ + ``); + + const member_iq = sent_IQs.pop(); + expect(Strophe.serialize(member_iq)).toBe( + ``+ + ``+ + ``); + view.close(); + + _converse.connection.IQ_stanzas = []; + sent_IQs = _converse.connection.IQ_stanzas; + api.settings.set('muc_fetch_members', false); + await mock.openAndEnterChatRoom(_converse, 'orchard@montague.lit', 'romeo'); + view = _converse.chatboxviews.get('orchard@montague.lit'); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(0); + await view.close(); + + _converse.connection.IQ_stanzas = []; + sent_IQs = _converse.connection.IQ_stanzas; + api.settings.set('muc_fetch_members', ['admin']); + await mock.openAndEnterChatRoom(_converse, 'courtyard@montague.lit', 'romeo'); + view = _converse.chatboxviews.get('courtyard@montague.lit'); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="admin"]')).length).toBe(1); + view.close(); + + _converse.connection.IQ_stanzas = []; + sent_IQs = _converse.connection.IQ_stanzas; + api.settings.set('muc_fetch_members', ['owner']); + await mock.openAndEnterChatRoom(_converse, 'garden@montague.lit', 'romeo'); + view = _converse.chatboxviews.get('garden@montague.lit'); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="owner"]')).length).toBe(1); + view.close(); + })); + + it("will not fetch the member list if the user is not affiliated", + mock.initConverse([], {'muc_fetch_members': true}, async function (_converse) { + + const muc_jid = 'lounge@montague.lit'; + const sent_IQs = _converse.connection.IQ_stanzas; + spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough(); + // Join MUC without an affiliation + const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], [], true, {}, 'none', 'participant'); + await u.waitUntil(() => model.occupants.fetchMembers.calls.count()); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(0); + })); + + describe("when fetching the member lists", function () { + + it("gracefully handles being forbidden from fetching the lists for certain affiliations", + mock.initConverse([], {'muc_fetch_members': true}, async function (_converse) { + + const sent_IQs = _converse.connection.IQ_stanzas; + const muc_jid = 'lounge@montague.lit'; + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_hidden', + 'muc_membersonly', + 'muc_passwordprotected', + Strophe.NS.MAM, + Strophe.NS.SID + ]; + const nick = 'romeo'; + await _converse.api.rooms.open(muc_jid); + await mock.getRoomFeatures(_converse, muc_jid, features); + await mock.waitForReservedNick(_converse, muc_jid, nick); + mock.receiveOwnMUCPresence(_converse, muc_jid, nick); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); + + // Check in reverse order that we requested all three lists + const owner_iq = sent_IQs.pop(); + expect(Strophe.serialize(owner_iq)).toBe( + ``+ + ``+ + ``); + const admin_iq = sent_IQs.pop(); + expect(Strophe.serialize(admin_iq)).toBe( + ``+ + ``+ + ``); + const member_iq = sent_IQs.pop(); + expect(Strophe.serialize(member_iq)).toBe( + ``+ + ``+ + ``); + + // It might be that the user is not allowed to fetch certain lists. + let err_stanza = u.toStanza( + ` + + `); + _converse.connection._dataRecv(mock.createRequest(err_stanza)); + + err_stanza = u.toStanza( + ` + + `); + _converse.connection._dataRecv(mock.createRequest(err_stanza)); + + // Now the service sends the member lists to the user + const member_list_stanza = $iq({ + 'from': muc_jid, + 'id': member_iq.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) + .c('item', { + 'affiliation': 'member', + 'jid': 'hag66@shakespeare.lit', + 'nick': 'thirdwitch', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(member_list_stanza)); + + await u.waitUntil(() => view.model.occupants.length > 1); + expect(view.model.occupants.length).toBe(2); + // The existing owner occupant should not have their + // affiliation removed due to the owner list + // not being returned (forbidden err). + expect(view.model.occupants.findWhere({'jid': _converse.bare_jid}).get('affiliation')).toBe('owner'); + expect(view.model.occupants.findWhere({'jid': 'hag66@shakespeare.lit'}).get('affiliation')).toBe('member'); + })); + }); + }); +}); + +describe("Someone being invited to a groupchat", function () { + + it("will first be added to the member list if the groupchat is members only", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + await mock.waitForRoster(_converse, 'current', 0); + spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough(); + const sent_IQs = _converse.connection.IQ_stanzas; + const muc_jid = 'coven@chat.shakespeare.lit'; + const nick = 'romeo'; + const room_creation_promise = _converse.api.rooms.open(muc_jid, {nick}); + + // Check that the groupchat queried for the features. + let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`)).pop()); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); + + // State that the chat is members-only via the features IQ + const view = _converse.chatboxviews.get(muc_jid); + const features_stanza = $iq({ + from: 'coven@chat.shakespeare.lit', + 'id': stanza.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'conference', + 'name': 'A Dark Cave', + 'type': 'text' + }).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + .c('feature', {'var': 'muc_membersonly'}).up(); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + const sent_stanzas = _converse.connection.sent_stanzas; + await u.waitUntil(() => sent_stanzas.filter(s => s.matches(`presence[to="${muc_jid}/${nick}"]`)).pop()); + expect(view.model.features.get('membersonly')).toBeTruthy(); + + await room_creation_promise; + await mock.createContacts(_converse, 'current'); + + let sent_stanza, sent_id; + spyOn(_converse.connection, 'send').and.callFake(function (stanza) { + if (stanza.nodeName === 'message') { + sent_id = stanza.getAttribute('id'); + sent_stanza = stanza; + } + }); + const invitee_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const reason = "Please join this groupchat"; + view.model.directInvite(invitee_jid, reason); + + // Check in reverse order that we requested all three lists + const owner_iq = sent_IQs.pop(); + expect(Strophe.serialize(owner_iq)).toBe( + ``+ + ``+ + ``); + + const admin_iq = sent_IQs.pop(); + expect(Strophe.serialize(admin_iq)).toBe( + ``+ + ``+ + ``); + + const member_iq = sent_IQs.pop(); + expect(Strophe.serialize(member_iq)).toBe( + ``+ + ``+ + ``); + + // Now the service sends the member lists to the user + const member_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': member_iq.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) + .c('item', { + 'affiliation': 'member', + 'jid': 'hag66@shakespeare.lit', + 'nick': 'thirdwitch', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(member_list_stanza)); + + const admin_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': admin_iq.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) + .c('item', { + 'affiliation': 'admin', + 'jid': 'wiccarocks@shakespeare.lit', + 'nick': 'secondwitch' + }); + _converse.connection._dataRecv(mock.createRequest(admin_list_stanza)); + + const owner_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': owner_iq.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) + .c('item', { + 'affiliation': 'owner', + 'jid': 'crone1@shakespeare.lit', + }); + _converse.connection._dataRecv(mock.createRequest(owner_list_stanza)); + + // Converse puts the user on the member list + stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/muc#admin"]`)).pop()); + expect(stanza.outerHTML, + ``+ + ``+ + ``+ + `Please join this groupchat`+ + ``+ + ``+ + ``); + + const result = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': stanza.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }); + _converse.connection._dataRecv(mock.createRequest(result)); + + await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count()); + + // Finally check that the user gets invited. + expect(Strophe.serialize(sent_stanza)).toBe( // Strophe adds the xmlns attr (although not in spec) + ``+ + ``+ + `` + ); + })); +}); diff --git a/src/plugins/muc-views/tests/muc.js b/src/plugins/muc-views/tests/muc.js index d90e91637..515acf9d5 100644 --- a/src/plugins/muc-views/tests/muc.js +++ b/src/plugins/muc-views/tests/muc.js @@ -1,12 +1,6 @@ /*global mock, converse */ -const $pres = converse.env.$pres; -const $iq = converse.env.$iq; -const $msg = converse.env.$msg; -const Strophe = converse.env.Strophe; -const Promise = converse.env.Promise; -const sizzle = converse.env.sizzle; -const u = converse.env.utils; +const { $pres, $iq, $msg, Strophe, Promise, sizzle, u } = converse.env; describe("Groupchats", function () { @@ -284,145 +278,6 @@ describe("Groupchats", function () { })); - describe("upon being entered", function () { - - it("will fetch the member list if muc_fetch_members is true", - mock.initConverse([], {'muc_fetch_members': true}, async function (_converse) { - - const { api } = _converse; - let sent_IQs = _converse.connection.IQ_stanzas; - const muc_jid = 'lounge@montague.lit'; - await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - let view = _converse.chatboxviews.get(muc_jid); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(3); - - // Check in reverse order that we requested all three lists - const owner_iq = sent_IQs.pop(); - expect(Strophe.serialize(owner_iq)).toBe( - ``+ - ``+ - ``); - - const admin_iq = sent_IQs.pop(); - expect(Strophe.serialize(admin_iq)).toBe( - ``+ - ``+ - ``); - - const member_iq = sent_IQs.pop(); - expect(Strophe.serialize(member_iq)).toBe( - ``+ - ``+ - ``); - view.close(); - - _converse.connection.IQ_stanzas = []; - sent_IQs = _converse.connection.IQ_stanzas; - api.settings.set('muc_fetch_members', false); - await mock.openAndEnterChatRoom(_converse, 'orchard@montague.lit', 'romeo'); - view = _converse.chatboxviews.get('orchard@montague.lit'); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(0); - await view.close(); - - _converse.connection.IQ_stanzas = []; - sent_IQs = _converse.connection.IQ_stanzas; - api.settings.set('muc_fetch_members', ['admin']); - await mock.openAndEnterChatRoom(_converse, 'courtyard@montague.lit', 'romeo'); - view = _converse.chatboxviews.get('courtyard@montague.lit'); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="admin"]')).length).toBe(1); - view.close(); - - _converse.connection.IQ_stanzas = []; - sent_IQs = _converse.connection.IQ_stanzas; - api.settings.set('muc_fetch_members', ['owner']); - await mock.openAndEnterChatRoom(_converse, 'garden@montague.lit', 'romeo'); - view = _converse.chatboxviews.get('garden@montague.lit'); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="owner"]')).length).toBe(1); - view.close(); - })); - - describe("when fetching the member lists", function () { - - it("gracefully handles being forbidden from fetching the lists for certain affiliations", - mock.initConverse([], {'muc_fetch_members': true}, async function (_converse) { - - const sent_IQs = _converse.connection.IQ_stanzas; - const muc_jid = 'lounge@montague.lit'; - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_hidden', - 'muc_membersonly', - 'muc_passwordprotected', - Strophe.NS.MAM, - Strophe.NS.SID - ]; - const nick = 'romeo'; - await _converse.api.rooms.open(muc_jid); - await mock.getRoomFeatures(_converse, muc_jid, features); - await mock.waitForReservedNick(_converse, muc_jid, nick); - mock.receiveOwnMUCPresence(_converse, muc_jid, nick); - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); - - // Check in reverse order that we requested all three lists - const owner_iq = sent_IQs.pop(); - expect(Strophe.serialize(owner_iq)).toBe( - ``+ - ``+ - ``); - const admin_iq = sent_IQs.pop(); - expect(Strophe.serialize(admin_iq)).toBe( - ``+ - ``+ - ``); - const member_iq = sent_IQs.pop(); - expect(Strophe.serialize(member_iq)).toBe( - ``+ - ``+ - ``); - - // It might be that the user is not allowed to fetch certain lists. - let err_stanza = u.toStanza( - ` - - `); - _converse.connection._dataRecv(mock.createRequest(err_stanza)); - - err_stanza = u.toStanza( - ` - - `); - _converse.connection._dataRecv(mock.createRequest(err_stanza)); - - // Now the service sends the member lists to the user - const member_list_stanza = $iq({ - 'from': muc_jid, - 'id': member_iq.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) - .c('item', { - 'affiliation': 'member', - 'jid': 'hag66@shakespeare.lit', - 'nick': 'thirdwitch', - 'role': 'participant' - }); - _converse.connection._dataRecv(mock.createRequest(member_list_stanza)); - - await u.waitUntil(() => view.model.occupants.length > 1); - expect(view.model.occupants.length).toBe(2); - // The existing owner occupant should not have their - // affiliation removed due to the owner list - // not being returned (forbidden err). - expect(view.model.occupants.findWhere({'jid': _converse.bare_jid}).get('affiliation')).toBe('owner'); - expect(view.model.occupants.findWhere({'jid': 'hag66@shakespeare.lit'}).get('affiliation')).toBe('member'); - })); - }); - }); - describe("topic", function () { it("is shown the header", mock.initConverse([], {}, async function (_converse) { @@ -3826,150 +3681,6 @@ describe("Groupchats", function () { })); }); - describe("Someone being invited to a groupchat", function () { - - it("will first be added to the member list if the groupchat is members only", - mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { - - await mock.waitForRoster(_converse, 'current', 0); - spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough(); - const sent_IQs = _converse.connection.IQ_stanzas; - const muc_jid = 'coven@chat.shakespeare.lit'; - const nick = 'romeo'; - const room_creation_promise = _converse.api.rooms.open(muc_jid, {nick}); - - // Check that the groupchat queried for the features. - let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`)).pop()); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); - - // State that the chat is members-only via the features IQ - const view = _converse.chatboxviews.get(muc_jid); - const features_stanza = $iq({ - from: 'coven@chat.shakespeare.lit', - 'id': stanza.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }) - .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'conference', - 'name': 'A Dark Cave', - 'type': 'text' - }).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - .c('feature', {'var': 'muc_hidden'}).up() - .c('feature', {'var': 'muc_temporary'}).up() - .c('feature', {'var': 'muc_membersonly'}).up(); - _converse.connection._dataRecv(mock.createRequest(features_stanza)); - const sent_stanzas = _converse.connection.sent_stanzas; - await u.waitUntil(() => sent_stanzas.filter(s => s.matches(`presence[to="${muc_jid}/${nick}"]`)).pop()); - expect(view.model.features.get('membersonly')).toBeTruthy(); - - await room_creation_promise; - await mock.createContacts(_converse, 'current'); - - let sent_stanza, sent_id; - spyOn(_converse.connection, 'send').and.callFake(function (stanza) { - if (stanza.nodeName === 'message') { - sent_id = stanza.getAttribute('id'); - sent_stanza = stanza; - } - }); - const invitee_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const reason = "Please join this groupchat"; - view.model.directInvite(invitee_jid, reason); - - // Check in reverse order that we requested all three lists - const owner_iq = sent_IQs.pop(); - expect(Strophe.serialize(owner_iq)).toBe( - ``+ - ``+ - ``); - - const admin_iq = sent_IQs.pop(); - expect(Strophe.serialize(admin_iq)).toBe( - ``+ - ``+ - ``); - - const member_iq = sent_IQs.pop(); - expect(Strophe.serialize(member_iq)).toBe( - ``+ - ``+ - ``); - - // Now the service sends the member lists to the user - const member_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': member_iq.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) - .c('item', { - 'affiliation': 'member', - 'jid': 'hag66@shakespeare.lit', - 'nick': 'thirdwitch', - 'role': 'participant' - }); - _converse.connection._dataRecv(mock.createRequest(member_list_stanza)); - - const admin_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': admin_iq.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) - .c('item', { - 'affiliation': 'admin', - 'jid': 'wiccarocks@shakespeare.lit', - 'nick': 'secondwitch' - }); - _converse.connection._dataRecv(mock.createRequest(admin_list_stanza)); - - const owner_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': owner_iq.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) - .c('item', { - 'affiliation': 'owner', - 'jid': 'crone1@shakespeare.lit', - }); - _converse.connection._dataRecv(mock.createRequest(owner_list_stanza)); - - // Converse puts the user on the member list - stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/muc#admin"]`)).pop()); - expect(stanza.outerHTML, - ``+ - ``+ - ``+ - `Please join this groupchat`+ - ``+ - ``+ - ``); - - const result = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': stanza.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }); - _converse.connection._dataRecv(mock.createRequest(result)); - - await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count()); - - // Finally check that the user gets invited. - expect(Strophe.serialize(sent_stanza)).toBe( // Strophe adds the xmlns attr (although not in spec) - ``+ - ``+ - `` - ); - })); - }); describe("The affiliations delta", function () { diff --git a/src/shared/tests/mock.js b/src/shared/tests/mock.js index 221af8454..5accac5f7 100644 --- a/src/shared/tests/mock.js +++ b/src/shared/tests/mock.js @@ -283,7 +283,7 @@ async function returnMemberLists (_converse, muc_jid, members=[], affiliations=[ return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve)); } -async function receiveOwnMUCPresence (_converse, muc_jid, nick) { +async function receiveOwnMUCPresence (_converse, muc_jid, nick, affiliation='owner', role='moderator') { const sent_stanzas = _converse.connection.sent_stanzas; await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop()); const presence = $pres({ @@ -291,17 +291,23 @@ async function receiveOwnMUCPresence (_converse, muc_jid, nick) { from: `${muc_jid}/${nick}`, id: u.getUniqueId() }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'owner', - jid: _converse.bare_jid, - role: 'moderator' - }).up() + .c('item').attrs({ affiliation, role, 'jid': _converse.bare_jid }).up() .c('status').attrs({code:'110'}); _converse.connection._dataRecv(createRequest(presence)); } -async function openAndEnterChatRoom (_converse, muc_jid, nick, features=[], members=[], force_open=true, settings={}) { +async function openAndEnterChatRoom ( + _converse, + muc_jid, + nick, + features=[], + members=[], + force_open=true, + settings={}, + own_affiliation='owner', + own_role='moderator', + ) { const { api } = _converse; muc_jid = muc_jid.toLowerCase(); const room_creation_promise = api.rooms.open(muc_jid, settings, force_open); @@ -310,7 +316,7 @@ async function openAndEnterChatRoom (_converse, muc_jid, nick, features=[], memb // The user has just entered the room (because join was called) // and receives their own presence from the server. // See example 24: https://xmpp.org/extensions/xep-0045.html#enter-pres - await receiveOwnMUCPresence(_converse, muc_jid, nick); + await receiveOwnMUCPresence(_converse, muc_jid, nick, own_affiliation, own_role); await room_creation_promise; const model = _converse.chatboxes.get(muc_jid); @@ -318,7 +324,10 @@ async function openAndEnterChatRoom (_converse, muc_jid, nick, features=[], memb const affs = api.settings.get('muc_fetch_members'); const all_affiliations = Array.isArray(affs) ? affs : (affs ? ['member', 'admin', 'owner'] : []); - await returnMemberLists(_converse, muc_jid, members, all_affiliations); + + if (['member', 'admin', 'owner'].includes(own_affiliation)) { + await returnMemberLists(_converse, muc_jid, members, all_affiliations); + } await model.messages.fetched; return model; }