2
0
Эх сурвалжийг харах

modtools: Add form to set affiliation

without having to first query the members list
JC Brand 4 жил өмнө
parent
commit
7d9ca26108

+ 3 - 49
src/headless/plugins/muc/muc.js

@@ -1,5 +1,5 @@
 import log from '../../log';
-import muc_utils from './utils.js';
+import { computeAffiliationsDelta, setAffiliation } from './utils.js';
 import p from '../../utils/parse-helpers';
 import sizzle from 'sizzle';
 import u from '../../utils/form';
@@ -1118,29 +1118,6 @@ const ChatRoomMixin = {
         this.features.save(attrs);
     },
 
-    /**
-     * Send IQ stanzas to the server to set an affiliation for
-     * the provided JIDs.
-     * See: https://xmpp.org/extensions/xep-0045.html#modifymember
-     *
-     * Prosody doesn't accept multiple JIDs' affiliations
-     * being set in one IQ stanza, so as a workaround we send
-     * a separate stanza for each JID.
-     * Related ticket: https://issues.prosody.im/345
-     *
-     * @private
-     * @method _converse.ChatRoom#setAffiliation
-     * @param { string } affiliation - The affiliation
-     * @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 { Promise } A promise which resolves and fails depending on the XMPP server response.
-     */
-    setAffiliation (affiliation, members) {
-        members = members.filter(m => m.affiliation === undefined || m.affiliation === affiliation);
-        return Promise.all(members.map(m => this.sendAffiliationIQ(affiliation, m)));
-    },
-
     /**
      * Given a <field> element, return a copy with a <value> child if
      * we can find a value for it in this rooms config.
@@ -1370,29 +1347,6 @@ const ChatRoomMixin = {
         return this.occupants.findWhere({ 'jid': _converse.bare_jid });
     },
 
-    /**
-     * Send an IQ stanza specifying an affiliation change.
-     * @private
-     * @method _converse.ChatRoom#
-     * @param { String } affiliation: affiliation
-     *     (could also be stored on the member object).
-     * @param { Object } member: Map containing the member's jid and
-     *     optionally a reason and affiliation.
-     */
-    sendAffiliationIQ (affiliation, member) {
-        const iq = $iq({ to: this.get('jid'), type: 'set' })
-            .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
-            .c('item', {
-                'affiliation': member.affiliation || affiliation,
-                'nick': member.nick,
-                'jid': member.jid
-            });
-        if (member.reason !== undefined) {
-            iq.c('reason', member.reason);
-        }
-        return api.sendIQ(iq);
-    },
-
     /**
      * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
      *
@@ -1407,7 +1361,7 @@ const ChatRoomMixin = {
      */
     setAffiliations (members) {
         const affiliations = [...new Set(members.map(m => m.affiliation))];
-        return Promise.all(affiliations.map(a => this.setAffiliation(a, members)));
+        return Promise.all(affiliations.map(a => setAffiliation(this.get('jid'), a, members)));
     },
 
     /**
@@ -1554,7 +1508,7 @@ const ChatRoomMixin = {
         const all_affiliations = ['member', 'admin', 'owner'];
         const aff_lists = await Promise.all(all_affiliations.map(a => this.getAffiliationList(a)));
         const old_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
-        await this.setAffiliations(muc_utils.computeAffiliationsDelta(true, false, members, old_members));
+        await this.setAffiliations(computeAffiliationsDelta(true, false, members, old_members));
         await this.occupants.fetchMembers();
     },
 

+ 136 - 45
src/headless/plugins/muc/utils.js

@@ -3,58 +3,149 @@
  * @license Mozilla Public License (MPLv2)
  * @description This is the MUC utilities module.
  */
+import log from '../../log';
+import sizzle from 'sizzle';
+import { Strophe, $iq } from 'strophe.js/src/strophe';
+import { _converse, api } from '../../core.js';
 import { difference, indexOf } from "lodash-es";
+import { getAttributes } from '@converse/headless/shared/parsers';
+
+
+/**
+ * Send an IQ stanza to the server asking for all groupchats
+ */
+export async function getMUCsForDomain (domain) {
+    const iq = $iq({
+        'to': domain,
+        'from': _converse.connection.jid,
+        'type': "get"
+    }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS});
+    let response;
+    try {
+        response = await api.sendIQ(iq);
+    } catch (e) {
+        log.error(e);
+    }
+    const rooms = response ? sizzle('query item', response) : [];
+    if (rooms.length) {
+        return rooms.map(getAttributes);
+    } else {
+        return [];
+    }
+}
+
+/**
+ * Send IQ stanzas to the server to set an affiliation for
+ * the provided JIDs.
+ * See: https://xmpp.org/extensions/xep-0045.html#modifymember
+ *
+ * Prosody doesn't accept multiple JIDs' affiliations
+ * being set in one IQ stanza, so as a workaround we send
+ * a separate stanza for each JID.
+ * Related ticket: https://issues.prosody.im/345
+ *
+ * @param { String } muc_jid: The JID of the MUC in which to set the affiliation
+ * @param { string } affiliation - The affiliation
+ * @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 { Promise } A promise which resolves and fails depending on the XMPP server response.
+ */
+export function setAffiliation (muc_jid, affiliation, members) {
+    members = members.filter(m => m.affiliation === undefined || m.affiliation === affiliation);
+    return Promise.all(members.map(m => sendAffiliationIQ(muc_jid, affiliation, m)));
+}
+
+/**
+ * Sets the given affiliation on all MUCs on the given domain.
+ * This does *not* set the affiliation for future MUCs yet to be created on the
+ * domain.
+ * @param { String } domain
+ * @param { string } affiliation - The affiliation
+ * @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 { Promise } A promise which resolves and fails depending on the XMPP server response.
+ */
+export async function setAffiliationForDomain (domain, affiliation, members) {
+    const mucs = await getMUCsForDomain(domain);
+    return Promise.all(mucs.map(m => setAffiliation(m.jid, affiliation, members)));
+}
+
+/**
+ * Send an IQ stanza specifying an affiliation change.
+ * @param { String } muc_jid: The JID of the MUC to which the IQ must be sent
+ * @param { String } affiliation: affiliation
+ *     (could also be stored on the member object).
+ * @param { Object } member: Map containing the member's jid and
+ *     optionally a reason and affiliation.
+ */
+export function sendAffiliationIQ (muc_jid, affiliation, member) {
+    const iq = $iq({ to: muc_jid, type: 'set' })
+        .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
+        .c('item', {
+            'affiliation': member.affiliation || affiliation,
+            'nick': member.nick,
+            'jid': member.jid
+        });
+    if (member.reason !== undefined) {
+        iq.c('reason', member.reason);
+    }
+    return api.sendIQ(iq);
+}
+
+/**
+ * Given two lists of objects with 'jid', 'affiliation' and
+ * 'reason' properties, return a new list containing
+ * those objects that are new, changed or removed
+ * (depending on the 'remove_absentees' boolean).
+ *
+ * The affiliations for new and changed members stay the
+ * same, for removed members, the affiliation is set to 'none'.
+ *
+ * The 'reason' property is not taken into account when
+ * comparing whether affiliations have been changed.
+ * @private
+ * @method muc_utils#computeAffiliationsDelta
+ * @param { boolean } exclude_existing - Indicates whether JIDs from
+ *      the new list which are also in the old list
+ *      (regardless of affiliation) should be excluded
+ *      from the delta. One reason to do this
+ *      would be when you want to add a JID only if it
+ *      doesn't have *any* existing affiliation at all.
+ * @param { boolean } remove_absentees - Indicates whether JIDs
+ *      from the old list which are not in the new list
+ *      should be considered removed and therefore be
+ *      included in the delta with affiliation set
+ *      to 'none'.
+ * @param { array } new_list - Array containing the new affiliations
+ * @param { array } old_list - Array containing the old affiliations
+ * @returns { array }
+ */
+function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
+    const new_jids = new_list.map(o => o.jid);
+    const old_jids = old_list.map(o => o.jid);
+    // Get the new affiliations
+    let delta = difference(new_jids, old_jids).map(jid => new_list[indexOf(new_jids, jid)]);
+    if (!exclude_existing) {
+        // Get the changed affiliations
+        delta = delta.concat(new_list.filter(item => {
+            const idx = indexOf(old_jids, item.jid);
+            return idx >= 0 ? (item.affiliation !== old_list[idx].affiliation) : false;
+        }));
+    }
+    if (remove_absentees) { // Get the removed affiliations
+        delta = delta.concat(difference(old_jids, new_jids).map(jid => ({'jid': jid, 'affiliation': 'none'})));
+    }
+    return delta;
+}
 
 /**
  * The MUC utils object. Contains utility functions related to multi-user chat.
  * @namespace muc_utils
  */
 const muc_utils = {
-    /**
-     * Given two lists of objects with 'jid', 'affiliation' and
-     * 'reason' properties, return a new list containing
-     * those objects that are new, changed or removed
-     * (depending on the 'remove_absentees' boolean).
-     *
-     * The affiliations for new and changed members stay the
-     * same, for removed members, the affiliation is set to 'none'.
-     *
-     * The 'reason' property is not taken into account when
-     * comparing whether affiliations have been changed.
-     * @private
-     * @method muc_utils#computeAffiliationsDelta
-     * @param { boolean } exclude_existing - Indicates whether JIDs from
-     *      the new list which are also in the old list
-     *      (regardless of affiliation) should be excluded
-     *      from the delta. One reason to do this
-     *      would be when you want to add a JID only if it
-     *      doesn't have *any* existing affiliation at all.
-     * @param { boolean } remove_absentees - Indicates whether JIDs
-     *      from the old list which are not in the new list
-     *      should be considered removed and therefore be
-     *      included in the delta with affiliation set
-     *      to 'none'.
-     * @param { array } new_list - Array containing the new affiliations
-     * @param { array } old_list - Array containing the old affiliations
-     * @returns { array }
-     */
-    computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
-        const new_jids = new_list.map(o => o.jid);
-        const old_jids = old_list.map(o => o.jid);
-        // Get the new affiliations
-        let delta = difference(new_jids, old_jids).map(jid => new_list[indexOf(new_jids, jid)]);
-        if (!exclude_existing) {
-            // Get the changed affiliations
-            delta = delta.concat(new_list.filter(item => {
-                const idx = indexOf(old_jids, item.jid);
-                return idx >= 0 ? (item.affiliation !== old_list[idx].affiliation) : false;
-            }));
-        }
-        if (remove_absentees) { // Get the removed affiliations
-            delta = delta.concat(difference(old_jids, new_jids).map(jid => ({'jid': jid, 'affiliation': 'none'})));
-        }
-        return delta;
-    }
+    computeAffiliationsDelta
 }
 
 export default muc_utils;

+ 6 - 6
src/modals/base.js

@@ -3,7 +3,6 @@ import log from "@converse/headless/log";
 import tpl_alert_component from "templates/alert.js";
 import { View } from '@converse/skeletor/src/view.js';
 import { api, converse } from "@converse/headless/core";
-import { render } from 'lit-html';
 
 const { sizzle } = converse.env;
 const u = converse.env.utils;
@@ -67,13 +66,14 @@ const BaseModal = View.extend({
             log.error("Could not find a .modal-alert element in the modal to show an alert message in!");
             return;
         }
+        this.timeout && clearTimeout(this.timeout);
         // FIXME: Instead of adding the alert imperatively, we should
         // find a way to let the modal rerender with an alert message
-        render(tpl_alert_component({'type': `alert-${type}`, 'message': message}), body);
-        const el = body.firstElementChild;
-        setTimeout(() => {
-            u.addClass('fade-out', el);
-            setTimeout(() => u.removeElement(el), 600);
+        const tp = tpl_alert_component({'type': `alert-${type}`, 'message': message});
+        body.innerHTML = u.getElementFromTemplateResult(tp).outerHTML;
+        this.timeout = setTimeout(() => {
+            u.addClass('fade-out', body.firstElementChild);
+            setTimeout(() => (body.innerHTML = ''), 600);
         }, 5000);
     },
 

+ 19 - 7
src/modals/moderator-tools.js

@@ -4,6 +4,7 @@ import tpl_moderator_tools_modal from "./templates/moderator-tools.js";
 import { AFFILIATIONS, ROLES } from "@converse/headless/plugins/muc/index.js";
 import { __ } from '../i18n';
 import { api, converse } from "@converse/headless/core";
+import { setAffiliation, setAffiliationForDomain } from '@converse/headless/plugins/muc/utils.js';
 
 const { Strophe, sizzle } = converse.env;
 const u = converse.env.utils;
@@ -45,6 +46,7 @@ export default BootstrapModal.extend({
     toHTML () {
         const occupant = this.chatroomview.model.occupants.findWhere({'jid': _converse.bare_jid});
         return tpl_moderator_tools_modal(Object.assign(this.model.toJSON(), {
+            'muc_domain': Strophe.getDomainFromJid(this.chatroomview.model.get('jid')),
             'affiliations_filter': this.affiliations_filter,
             'assignAffiliation': ev => this.assignAffiliation(ev),
             'assignRole': ev => this.assignRole(ev),
@@ -147,7 +149,7 @@ export default BootstrapModal.extend({
         this.model.set({'affiliation': affiliation});
     },
 
-    async assignAffiliation (ev) {
+    async assignAffiliation(ev, refresh=false) {
         ev.stopPropagation();
         ev.preventDefault();
         const data = new FormData(ev.target);
@@ -156,9 +158,14 @@ export default BootstrapModal.extend({
             'jid': data.get('jid'),
             'reason': data.get('reason')
         }
-        const current_affiliation = this.model.get('affiliation');
+        const muc_jid = this.chatroomview.model.get('jid');
+        const muc_domain = Strophe.getDomainFromJid(this.chatroomview.model.get('jid'));
         try {
-            await this.chatroomview.model.setAffiliation(affiliation, [attrs]);
+            if (data.get('allmucs')) {
+                await setAffiliationForDomain(muc_domain, affiliation, [attrs]);
+            } else {
+                await setAffiliation(muc_jid, affiliation, [attrs]);
+            }
         } catch (e) {
             if (e === null) {
                 this.alert(__('Timeout error while trying to set the affiliation'), 'danger');
@@ -170,10 +177,15 @@ export default BootstrapModal.extend({
             log.error(e);
             return;
         }
-        this.alert(__('Affiliation changed'), 'primary');
-        await this.chatroomview.model.occupants.fetchMembers()
-        this.model.set({'affiliation': null}, {'silent': true});
-        this.model.set({'affiliation': current_affiliation});
+        this.alert(__('Affiliation changed to %1$s', affiliation), 'primary');
+        if (refresh) {
+            await this.chatroomview.model.occupants.fetchMembers()
+            const current_affiliation = this.model.get('affiliation');
+            if (current_affiliation) {
+                this.model.set({'affiliation': null}, {'silent': true});
+                this.model.set({'affiliation': current_affiliation});
+            }
+        }
     },
 
     assignRole (ev) {

+ 46 - 5
src/modals/templates/moderator-tools.js

@@ -86,31 +86,59 @@ const role_list_item = (o) => html`
     </li>
 `;
 
+const tpl_jid = () => {
+    const i18n_address = __('XMPP Address');
+    const i18n_example_jid = __('user@example.org');
+    return html`
+        <div class="form-group">
+            <div class="row">
+                <div class="col">
+                    <label><strong>${i18n_address}:</strong></label>
+                    <input required class="form-control" type="text" name="jid" placeholder="${i18n_example_jid}"/>
+                </div>
+            </div>
+        </div>
+    `;
+}
+
 
 const tpl_set_affiliation_form = (o) => {
     const i18n_change_affiliation = __('Change affiliation');
     const i18n_new_affiliation = __('New affiliation');
     const i18n_reason = __('Reason');
+    const i18n_set_affiation = __('Set affiliation by XMPP address');
+    const form_classes = `affiliation-form ${o.item ? 'hidden' : ''}`;
+    const i18n_assign_all_mucs = __('Assign in all groupchats on %1$s', o.muc_domain);
     return html`
-        <form class="affiliation-form hidden" @submit=${o.assignAffiliation}>
-            <div class="form-group">
+        ${ o.item ? html`<h6 class="centered">${i18n_set_affiation}</h6>` : '' }
+        <form class="${form_classes}" @submit=${ev => o.assignAffiliation(ev, !!o.item)}>
+            ${ o.item ? html`
                 <input type="hidden" name="jid" value="${o.item.jid}"/>
                 <input type="hidden" name="nick" value="${o.item.nick}"/>
+            ` : tpl_jid() }
+            <div class="form-group">
                 <div class="row">
                     <div class="col">
                         <label><strong>${i18n_new_affiliation}:</strong></label>
                         <select class="custom-select select-affiliation" name="affiliation">
-                            ${ o.assignable_affiliations.map(aff => html`<option value="${aff}" ?selected=${aff === o.item.affiliation}>${aff}</option>`) }
+                            ${ o.assignable_affiliations.map(aff => html`<option value="${aff}" ?selected=${aff === o.item?.affiliation}>${aff}</option>`) }
                         </select>
                     </div>
                     <div class="col">
+                        <p>
                         <label><strong>${i18n_reason}:</strong></label>
                         <input class="form-control" type="text" name="reason"/>
+                        </p>
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="col">
+                        <label><input type="checkbox" name="allmucs"/> &nbsp; ${i18n_assign_all_mucs}</label>
                     </div>
                 </div>
             </div>
             <div class="form-group">
-                <input type="submit" class="btn btn-primary" name="change" value="${i18n_change_affiliation}"/>
+                <input type="submit" class="centered btn btn-primary" name="change" value="${i18n_change_affiliation}"/>
             </div>
         </form>
     `;
@@ -165,7 +193,13 @@ export default (o) => {
         "grants privileges and responsibilities. For example admins and owners automatically have the "+
         "moderator role."
     );
+    const i18n_helptext_query_affiliation = __(
+        "Be careful when querying in very large groupchats with thousands of affiliated users your browser "+
+        "may become unresponsive."
+    );
     const show_both_tabs = o.queryable_roles.length && o.queryable_affiliations.length;
+    const i18n_set_affiliation = __('Set affiliation for a single user');
+    const i18n_query_affiliation = __('Query for users by affiliation');
     return html`
     <div class="modal-dialog" role="document">
         <div class="modal-content">
@@ -180,8 +214,15 @@ export default (o) => {
 
                 <div class="tab-content">
                     <div class="tab-pane tab-pane--columns ${ o.queryable_affiliations.length ? 'active' : ''}" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
+                        <br/>
+                        <p class="helptext pb-3">${i18n_helptext_affiliation}</p>
+                        <hr/>
+                        <h6 class="centered">${i18n_set_affiliation}</h6>
+                        <p class="helptext pb-3">${i18n_helptext_query_affiliation}</p>
+                        ${ tpl_set_affiliation_form(o) }
+                        <hr/>
+                        <h6 class="centered">${i18n_query_affiliation}</h6>
                         <form class="converse-form query-affiliation" @submit=${o.queryAffiliation}>
-                            <p class="helptext pb-3">${i18n_helptext_affiliation}</p>
                             <div class="form-group">
                                 <label for="affiliation">
                                     <strong>${i18n_affiliation}:</strong>

+ 2 - 2
webpack.html

@@ -26,8 +26,8 @@
         auto_away: 300,
         auto_register_muc_nickname: true,
         loglevel: 'debug',
-        modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'],
-        modtools_disable_query: ['moderator', 'participant', 'visitor'],
+        // modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'],
+        // modtools_disable_query: ['moderator', 'participant', 'visitor'],
         enable_smacks: true,
         // connection_options: { 'worker': '/dist/shared-connection-worker.js' },
         message_archiving: 'always',