Pārlūkot izejas kodu

New config setting: `muc_fetch_members`

JC Brand 6 gadi atpakaļ
vecāks
revīzija
c4ad02d4e3
6 mainītis faili ar 126 papildinājumiem un 56 dzēšanām
  1. 1 0
      CHANGES.md
  2. 23 0
      docs/source/configuration.rst
  3. 61 27
      spec/muc.js
  4. 35 26
      src/headless/converse-muc.js
  5. 3 2
      src/headless/utils/muc.js
  6. 3 1
      tests/utils.js

+ 1 - 0
CHANGES.md

@@ -21,6 +21,7 @@
 - New config option [clear_messages_on_reconnection](https://conversejs.org/docs/html/configuration.html#clear-messages-on-reconnection)
 - New config option [enable_smacks](https://conversejs.org/docs/html/configuration.html#enable-smacks)
 - New config option [message_limit](https://conversejs.org/docs/html/configuration.html#message-limit)
+- New config option [muc_fetch_members](https://conversejs.org/docs/html/configuration.html#muc-fetch-members)
 - New config option [muc_mention_autocomplete_min_chars](https://conversejs.org/docs/html/configuration.html#muc-mention-autocomplete-min-chars)
 - New config option [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status)
 - New config option [singleton](https://conversejs.org/docs/html/configuration.html#singleton)

+ 23 - 0
docs/source/configuration.rst

@@ -451,6 +451,7 @@ Example:
 
 .. _`bosh-service-url`:
 
+
 bosh_service_url
 ----------------
 
@@ -954,6 +955,28 @@ other domains.
 If you want to restrict MUCs to only this domain, then set `locked_domain`_ to
 ``true``.
 
+
+muc_fetch_members
+-----------------
+
+* Default:  ``true``
+
+Determines whether Converse.js will fetch the member lists for a MUC
+(multi-user chat) when the user first enters it.
+
+Here's the relevant part from the MUC XEP: https://xmpp.org/extensions/xep-0045.html#getmemberlist
+
+The MUC service won't necessarily allow any user to fetch member lists,
+but can usually be configured to do so.
+
+The member lists consists of three lists of users who have the affiliations
+``member``, ``admin`` and ``owner`` respectively.
+
+By fetching member lists, Converse.js will always show these users as
+participants of the MUC, which makes it behave a bit more like modern chat
+apps.
+
+
 muc_history_max_stanzas
 -----------------------
 

+ 61 - 27
spec/muc.js

@@ -400,13 +400,34 @@
 
         describe("A Groupchat", function () {
 
+
+            describe("upon being entered", function () {
+                it("will fetch the member list if muc_fetch_members is true",
+                    mock.initConverse(
+                        null, ['rosterGroupsFetched'], {'muc_fetch_members': true},
+                        async function (done, _converse) {
+
+                    spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough();
+                    await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
+                    let view = _converse.chatboxviews.get('lounge@montague.lit');
+                    expect(view.model.occupants.fetchMembers).toHaveBeenCalled();
+
+                    _converse.muc_fetch_members = false;
+                    await test_utils.openAndEnterChatRoom(_converse, 'orchard@montague.lit', 'romeo');
+                    view = _converse.chatboxviews.get('orchard@montague.lit');
+                    expect(view.model.occupants.fetchMembers.calls.count()).toBe(1);
+                    done();
+                }));
+            });
+
             it("clears cached messages when it gets closed",
                 mock.initConverse(
                     null, ['rosterGroupsFetched'], {},
                     async function (done, _converse) {
 
-                await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
-                const view = _converse.chatboxviews.get('lounge@montague.lit');
+                const muc_jid = 'lounge@montague.lit';
+                await test_utils.openAndEnterChatRoom(_converse, muc_jid , 'romeo');
+                const view = _converse.chatboxviews.get(muc_jid);
                 const message = 'Hello world',
                         nick = mock.chatroom_names[0],
                         msg = $msg({
@@ -4185,14 +4206,10 @@
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     async function (done, _converse) {
 
-                var sent_IQs = [], IQ_ids = [];
+                spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough();
                 const sendIQ = _converse.connection.sendIQ;
                 const IQ_stanzas = _converse.connection.IQ_stanzas;
                 const muc_jid = 'coven@chat.shakespeare.lit';
-                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                    sent_IQs.push(iq);
-                    IQ_ids.push(sendIQ.bind(this)(iq, callback, errback));
-                });
 
                 await _converse.api.rooms.open(muc_jid, {'nick': 'romeo'});
                 let stanza = await u.waitUntil(() => _.filter(
@@ -4206,11 +4223,18 @@
                         `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
                     `</iq>`);
 
-                const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
+                const sent_IQs = _converse.connection.IQ_stanzas;
+                const last_sent_IQ = sent_IQs.pop();
+                expect(Strophe.serialize(last_sent_IQ)).toBe(
+                    `<iq from="romeo@montague.lit/orchard" id="${last_sent_IQ.getAttribute('id')}" to="coven@chat.shakespeare.lit" type="get" xmlns="jabber:client">`+
+                        `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
+                    `</iq>`);
+
+                const view = _converse.chatboxviews.get(muc_jid);
                 // State that the chat is members-only via the features IQ
                 const features_stanza = $iq({
                         from: 'coven@chat.shakespeare.lit',
-                        'id': IQ_ids.pop(),
+                        'id': last_sent_IQ.getAttribute('id'),
                         'to': 'romeo@montague.lit/desktop',
                         'type': 'result'
                     })
@@ -4244,24 +4268,23 @@
 
                 // Check in reverse order that we requested all three lists
                 // (member, owner and admin).
-                const admin_iq_id = IQ_ids.pop();
-                const owner_iq_id = IQ_ids.pop();
-                const member_iq_id = IQ_ids.pop();
-
-                expect(sent_IQs.pop().toLocaleString()).toBe(
-                    `<iq id="${admin_iq_id}" to="coven@chat.shakespeare.lit" type="get" xmlns="jabber:client">`+
+                const admin_iq = sent_IQs.pop();
+                const owner_iq = sent_IQs.pop();
+                const member_iq = sent_IQs.pop();
+                expect(Strophe.serialize(admin_iq)).toBe(
+                    `<iq id="${admin_iq.getAttribute('id')}" to="coven@chat.shakespeare.lit" type="get" xmlns="jabber:client">`+
                         `<query xmlns="http://jabber.org/protocol/muc#admin">`+
                             `<item affiliation="admin"/>`+
                         `</query>`+
                     `</iq>`);
-                expect(sent_IQs.pop().toLocaleString()).toBe(
-                    `<iq id="${owner_iq_id}" to="coven@chat.shakespeare.lit" type="get" xmlns="jabber:client">`+
+                expect(Strophe.serialize(owner_iq)).toBe(
+                    `<iq id="${owner_iq.getAttribute('id')}" to="coven@chat.shakespeare.lit" type="get" xmlns="jabber:client">`+
                         `<query xmlns="http://jabber.org/protocol/muc#admin">`+
                             `<item affiliation="owner"/>`+
                         `</query>`+
                     `</iq>`);
-                expect(sent_IQs.pop().toLocaleString()).toBe(
-                    `<iq id="${member_iq_id}" to="coven@chat.shakespeare.lit" type="get" xmlns="jabber:client">`+
+                expect(Strophe.serialize(member_iq)).toBe(
+                    `<iq id="${member_iq.getAttribute('id')}" to="coven@chat.shakespeare.lit" type="get" xmlns="jabber:client">`+
                         `<query xmlns="http://jabber.org/protocol/muc#admin">`+
                             `<item affiliation="member"/>`+
                         `</query>`+
@@ -4281,9 +4304,9 @@
                  *  </query>
                  *  </iq>
                  */
-                var member_list_stanza = $iq({
+                const member_list_stanza = $iq({
                         'from': 'coven@chat.shakespeare.lit',
-                        'id': member_iq_id,
+                        'id': member_iq.getAttribute('id'),
                         'to': 'romeo@montague.lit/orchard',
                         'type': 'result'
                     }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN})
@@ -4295,9 +4318,9 @@
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(member_list_stanza));
 
-                var admin_list_stanza = $iq({
+                const admin_list_stanza = $iq({
                         'from': 'coven@chat.shakespeare.lit',
-                        'id': admin_iq_id,
+                        'id': admin_iq.getAttribute('id'),
                         'to': 'romeo@montague.lit/orchard',
                         'type': 'result'
                     }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN})
@@ -4308,9 +4331,9 @@
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(admin_list_stanza));
 
-                var owner_list_stanza = $iq({
+                const owner_list_stanza = $iq({
                         'from': 'coven@chat.shakespeare.lit',
-                        'id': owner_iq_id,
+                        'id': owner_iq.getAttribute('id'),
                         'to': 'romeo@montague.lit/orchard',
                         'type': 'result'
                     }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN})
@@ -4319,20 +4342,31 @@
                             'jid': 'crone1@shakespeare.lit',
                         });
                 _converse.connection._dataRecv(test_utils.createRequest(owner_list_stanza));
-                await u.waitUntil(() => IQ_ids.length, 300);
+
                 stanza = await u.waitUntil(() => _.filter(
                     IQ_stanzas,
                     iq => iq.querySelector(
                         `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/muc#admin"]`
                     )).pop());
                 expect(stanza.outerHTML,
-                    `<iq id="${IQ_ids.pop()}" to="coven@chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
+                    `<iq id="${stanza.getAttribute('id')}" to="coven@chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
                         `<query xmlns="http://jabber.org/protocol/muc#admin">`+
                             `<item affiliation="member" jid="${invitee_jid}">`+
                                 `<reason>Please join this groupchat</reason>`+
                             `</item>`+
                         `</query>`+
                     `</iq>`);
+
+                const result = $iq({
+                        'from': 'coven@chat.shakespeare.lit',
+                        'id': stanza.getAttribute('id'),
+                        'to': 'romeo@montague.lit/orchard',
+                        'type': 'result'
+                    });
+                _converse.connection._dataRecv(test_utils.createRequest(result));
+
+                await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count());
+
                 // Finally check that the user gets invited.
                 expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
                     `<message from="romeo@montague.lit/orchard" id="${sent_id}" to="${invitee_jid}" xmlns="jabber:client">`+

+ 35 - 26
src/headless/converse-muc.js

@@ -116,6 +116,7 @@ converse.plugins.add('converse-muc', {
             'auto_register_muc_nickname': false,
             'locked_muc_domain': false,
             'muc_domain': undefined,
+            'muc_fetch_members': true,
             'muc_history_max_stanzas': undefined,
             'muc_instant_rooms': true,
             'muc_nickname_from_jid': false
@@ -417,7 +418,9 @@ converse.plugins.add('converse-muc', {
 
             async onConnectionStatusChanged () {
                 if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
-                    await this.occupants.fetchMembers();
+                    if (_converse.muc_fetch_members) {
+                        await this.occupants.fetchMembers();
+                    }
                     // It's possible to fetch messages before entering a MUC,
                     // but we don't support this use-case currently. By
                     // fetching messages after members we can immediately
@@ -856,18 +859,11 @@ converse.plugins.add('converse-muc', {
              * @param { object } members - A map of jids, affiliations and
              *      optionally reasons. Only those entries with the
              *      same affiliation as being currently set will be considered.
-             * @returns
-             *  A promise which resolves and fails depending on the XMPP server response.
+             * @returns { Promise } A promise which resolves and fails depending on the XMPP server response.
              */
             setAffiliation (affiliation, members) {
-                members = _.filter(members, (member) =>
-                    // We only want those members who have the right
-                    // affiliation (or none, which implies the provided one).
-                    _.isUndefined(member.affiliation) ||
-                            member.affiliation === affiliation
-                );
-                const promises = _.map(members, _.bind(this.sendAffiliationIQ, this, affiliation));
-                return Promise.all(promises);
+                members = members.filter(m => _.isUndefined(m.affiliation) || m.affiliation === affiliation);
+                return Promise.all(members.map(m => this.sendAffiliationIQ(affiliation, m)));
             },
 
             /**
@@ -1050,18 +1046,20 @@ converse.plugins.add('converse-muc', {
             },
 
             /**
-             * Send IQ stanzas to the server to modify the
-             * affiliations in this groupchat.
+             * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
+             *
              * See: https://xmpp.org/extensions/xep-0045.html#modifymember
              * @private
              * @method _converse.ChatRoom#setAffiliations
-             * @param { object } members - A map of jids, affiliations and optionally reasons
-             * @param { function } onSuccess - callback for a succesful response
-             * @param { function } onError - callback for an error response
+             * @param { Object[] } members
+             * @param { string } members[].jid - The JID of the user whose affiliation will change
+             * @param { Array } members[].affiliation - The new affiliation for this user
+             * @param { string } [members[].reason] - An optional reason for the affiliation change
+             * @returns { Promise }
              */
             setAffiliations (members) {
-                const affiliations = _.uniq(_.map(members, 'affiliation'));
-                return Promise.all(_.map(affiliations, _.partial(this.setAffiliation.bind(this), _, members)));
+                const affiliations = _.uniq(members.map(m => m.affiliation));
+                return Promise.all(affiliations.map(a => this.setAffiliation(a, members)));
             },
 
             /**
@@ -1101,10 +1099,15 @@ converse.plugins.add('converse-muc', {
                     this.occupants.findWhere({'nick': nick_or_jid});
             },
 
+            /**
+             * Returns a map of JIDs that have the affiliations
+             * as provided.
+             * @private
+             * @method _converse.ChatRoom#getJidsWithAffiliations
+             * @param { string|array } affiliation - An array of affiliations or
+             *      a string if only one affiliation.
+             */
             async getJidsWithAffiliations (affiliations) {
-                /* Returns a map of JIDs that have the affiliations
-                 * as provided.
-                 */
                 if (_.isString(affiliations)) {
                     affiliations = [affiliations];
                 }
@@ -1135,11 +1138,17 @@ converse.plugins.add('converse-muc', {
              *  updated or once it's been established there's no need
              *  to update the list.
              */
-            updateMemberLists (members, affiliations, deltaFunc) {
-                this.getJidsWithAffiliations(affiliations)
-                    .then(old_members => this.setAffiliations(deltaFunc(members, old_members)))
-                    .then(() => this.occupants.fetchMembers())
-                    .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+            async updateMemberLists (members, affiliations, deltaFunc) {
+                try {
+                    const old_members = await this.getJidsWithAffiliations(affiliations);
+                    await this.setAffiliations(deltaFunc(members, old_members));
+                } catch (e) {
+                    _converse.log(e, Strophe.LogLevel.ERROR);
+                    return;
+                }
+                if (_converse.muc_fetch_members) {
+                    return this.occupants.fetchMembers();
+                }
             },
 
             /**

+ 3 - 2
src/headless/utils/muc.js

@@ -39,10 +39,11 @@ const { Strophe, sizzle, _ } = converse.env;
  *      to 'none'.
  * @param { array } new_list - Array containing the new affiliations
  * @param { array } old_list - Array containing the old affiliations
+ * @returns { array }
  */
 u.computeAffiliationsDelta = function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
-    const new_jids = _.map(new_list, 'jid');
-    const old_jids = _.map(old_list, 'jid');
+    const new_jids = new_list.map(o => o.jid);
+    const old_jids = old_list.map(o => o.jid);
 
     // Get the new affiliations
     let delta = _.map(

+ 3 - 1
tests/utils.js

@@ -287,7 +287,9 @@
 
         const view = _converse.chatboxviews.get(muc_jid);
         await u.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED));
-        await utils.returnMemberLists(_converse, muc_jid, members);
+        if (_converse.muc_fetch_members) {
+            await utils.returnMemberLists(_converse, muc_jid, members);
+        }
     };
 
     utils.clearBrowserStorage = function () {