Extract affiliation-related methods out of the ChatRoom model

and put them together in a utils file
This commit is contained in:
JC Brand 2021-04-13 14:10:59 +02:00
parent 13e19eb7f8
commit 383f5c1d60
11 changed files with 198 additions and 192 deletions

View File

@ -323,13 +323,13 @@ window.addEventListener('converse-loaded', () => {
await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
await room_creation_promise;
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
const model = _converse.chatboxes.get(muc_jid);
await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
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 view.model.messages.fetched;
return model.messages.fetched;
};
mock.createContact = async function (_converse, name, ask, requesting, subscription) {

View File

@ -0,0 +1,153 @@
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import log from "@converse/headless/log";
import { api, converse } from '@converse/headless/core.js';
import { difference, indexOf } from 'lodash-es';
import { parseMemberListIQ } from '../parsers.js';
const { Strophe, $iq, u } = converse.env;
/**
* Sends an IQ stanza to the server, asking it for the relevant affiliation list .
* Returns an array of {@link MemberListItem} objects, representing occupants
* that have the given affiliation.
* See: https://xmpp.org/extensions/xep-0045.html#modifymember
* @param { ("admin"|"owner"|"member") } affiliation
* @param { String } muc_jid - The JID of the MUC for which the affiliation list should be fetched
* @returns { Promise<MemberListItem[]> }
*/
export async function getAffiliationList (affiliation, muc_jid) {
const iq = $iq({ 'to': muc_jid, 'type': 'get' })
.c('query', { xmlns: Strophe.NS.MUC_ADMIN })
.c('item', { 'affiliation': affiliation });
const result = await api.sendIQ(iq, null, false);
if (result === null) {
const err_msg = `Error: timeout while fetching ${affiliation} list for MUC ${muc_jid}`;
const err = new Error(err_msg);
log.warn(err_msg);
log.warn(result);
return err;
}
if (u.isErrorStanza(result)) {
const err_msg = `Error: not allowed to fetch ${affiliation} list for MUC ${muc_jid}`;
const err = new Error(err_msg);
log.warn(err_msg);
log.warn(result);
return err;
}
return parseMemberListIQ(result)
.filter(p => p)
.sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
}
/**
* Send IQ stanzas to the server to modify affiliations for users in this groupchat.
* See: https://xmpp.org/extensions/xep-0045.html#modifymember
* @param { Object[] } users
* @param { string } users[].jid - The JID of the user whose affiliation will change
* @param { Array } users[].affiliation - The new affiliation for this user
* @param { string } [users[].reason] - An optional reason for the affiliation change
* @returns { Promise }
*/
export function setAffiliations (muc_jid, users) {
const affiliations = [...new Set(users.map(u => u.affiliation))];
return Promise.all(affiliations.map(a => setAffiliation(a, muc_jid, users)));
}
/**
* 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 } affiliation - The affiliation
* @param { String|Array<String> } jids - The JID(s) of the MUCs in which the
* affiliations need to be set.
* @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 (affiliation, muc_jids, members) {
if (!Array.isArray(muc_jids)) {
muc_jids = [muc_jids];
}
members = members.filter(m => [undefined, affiliation].includes(m.affiliation));
return Promise.all(
muc_jids.reduce((acc, jid) => [...acc, ...members.map(m => sendAffiliationIQ(affiliation, jid, m))], [])
);
}
/**
* Send an IQ stanza specifying an affiliation change.
* @private
* @param { String } affiliation: affiliation (could also be stored on the member object).
* @param { String } muc_jid: The JID of the MUC in which the affiliation should be set.
* @param { Object } member: Map containing the member's jid and optionally a reason and affiliation.
*/
function sendAffiliationIQ (affiliation, muc_jid, 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.
* @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 }
*/
export 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;
}

View File

@ -13,7 +13,7 @@ import ChatRoomOccupant from './occupant.js';
import ChatRoomOccupants from './occupants.js';
import log from '../../log';
import muc_api from './api.js';
import muc_utils from './utils.js';
import { computeAffiliationsDelta } from './affiliations/utils.js';
import u from '../../utils/form';
import { Collection } from '@converse/skeletor/src/collection';
import { _converse, api, converse } from '../../core.js';
@ -265,7 +265,8 @@ converse.plugins.add('converse-muc', {
);
}
converse.env.muc_utils = muc_utils;
// This is for tests (at least until we can import modules inside tests)
converse.env.muc_utils = { computeAffiliationsDelta };
Object.assign(api, muc_api);
/* https://xmpp.org/extensions/xep-0045.html

View File

@ -1,14 +1,14 @@
import log from '../../log';
import muc_utils from './utils.js';
import p from '../../utils/parse-helpers';
import sizzle from 'sizzle';
import u from '../../utils/form';
import { Model } from '@converse/skeletor/src/model.js';
import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
import { _converse, api, converse } from '../../core.js';
import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
import { debounce, intersection, invoke, isElement, pick, zipObject } from 'lodash-es';
import { isArchived } from '@converse/headless/shared/parsers';
import { parseMemberListIQ, parseMUCMessage, parseMUCPresence } from './parsers.js';
import { parseMUCMessage, parseMUCPresence } from './parsers.js';
import { sendMarker } from '@converse/headless/shared/actions';
const OWNER_COMMANDS = ['owner'];
@ -1136,29 +1136,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.
@ -1388,46 +1365,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.
*
* See: https://xmpp.org/extensions/xep-0045.html#modifymember
* @private
* @method _converse.ChatRoom#setAffiliations
* @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 = [...new Set(members.map(m => m.affiliation))];
return Promise.all(affiliations.map(a => this.setAffiliation(a, members)));
},
/**
* Send an IQ stanza to modify an occupant's role
* @private
@ -1521,40 +1458,6 @@ const ChatRoomMixin = {
);
},
/**
* Sends an IQ stanza to the server, asking it for the relevant affiliation list .
* Returns an array of {@link MemberListItem} objects, representing occupants
* that have the given affiliation.
* See: https://xmpp.org/extensions/xep-0045.html#modifymember
* @private
* @method _converse.ChatRoom#getAffiliationList
* @param { ("admin"|"owner"|"member") } affiliation
* @returns { Promise<MemberListItem[]> }
*/
async getAffiliationList (affiliation) {
const iq = $iq({ to: this.get('jid'), type: 'get' })
.c('query', { xmlns: Strophe.NS.MUC_ADMIN })
.c('item', { 'affiliation': affiliation });
const result = await api.sendIQ(iq, null, false);
if (result === null) {
const err_msg = `Error: timeout while fetching ${affiliation} list for MUC ${this.get('jid')}`;
const err = new Error(err_msg);
log.warn(err_msg);
log.warn(result);
return err;
}
if (u.isErrorStanza(result)) {
const err_msg = `Error: not allowed to fetch ${affiliation} list for MUC ${this.get('jid')}`;
const err = new Error(err_msg);
log.warn(err_msg);
log.warn(result);
return err;
}
return parseMemberListIQ(result)
.filter(p => p)
.sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
},
/**
* Fetch the lists of users with the given affiliations.
* Then compute the delta between those users and
@ -1569,10 +1472,11 @@ const ChatRoomMixin = {
* to update the list.
*/
async updateMemberLists (members) {
const muc_jid = this.get('jid');
const all_affiliations = ['member', 'admin', 'owner'];
const aff_lists = await Promise.all(all_affiliations.map(a => this.getAffiliationList(a)));
const aff_lists = await Promise.all(all_affiliations.map(a => getAffiliationList(a, muc_jid)));
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 setAffiliations(muc_jid, computeAffiliationsDelta(true, false, members, old_members));
await this.occupants.fetchMembers();
},

View File

@ -3,6 +3,7 @@ import u from '../../utils/form';
import { Collection } from '@converse/skeletor/src/collection';
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,
@ -43,7 +44,8 @@ const ChatRoomOccupants = Collection.extend({
if (affiliations.length === 0) {
return;
}
const aff_lists = await Promise.all(affiliations.map(a => this.chatroom.getAffiliationList(a)));
const muc_jid = this.chatroom.get('jid');
const aff_lists = await Promise.all(affiliations.map(a => getAffiliationList(a, muc_jid)));
const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
const known_affiliations = affiliations.filter(
a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)])

View File

@ -1,60 +0,0 @@
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description This is the MUC utilities module.
*/
import { difference, indexOf } from "lodash-es";
/**
* 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;
}
}
export default muc_utils;

View File

@ -1,9 +1,10 @@
import BootstrapModal from "./base.js";
import BootstrapModal from "modals/base.js";
import log from "@converse/headless/log";
import tpl_moderator_tools_modal from "./templates/moderator-tools.js";
import tpl_moderator_tools_modal from "../templates/moderator-tools.js";
import { AFFILIATIONS, ROLES } from "@converse/headless/plugins/muc/index.js";
import { __ } from '../i18n';
import { __ } from 'i18n';
import { api, converse } from "@converse/headless/core";
import { getAffiliationList, setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js'
const { Strophe, sizzle } = converse.env;
const u = converse.env.utils;
@ -33,7 +34,8 @@ export default BootstrapModal.extend({
const chatroom = this.chatroomview.model;
const affiliation = this.model.get('affiliation');
if (this.shouldFetchAffiliationsList()) {
this.users_with_affiliation = await chatroom.getAffiliationList(affiliation);
const muc_jid = chatroom.get('jid');
this.users_with_affiliation = await getAffiliationList(affiliation, muc_jid);
} else {
this.users_with_affiliation = chatroom.getOccupantsWithAffiliation(affiliation);
}
@ -157,8 +159,9 @@ export default BootstrapModal.extend({
'reason': data.get('reason')
}
const current_affiliation = this.model.get('affiliation');
const muc_jid = this.chatroomview.model.get('jid');
try {
await this.chatroomview.model.setAffiliation(affiliation, [attrs]);
await setAffiliation(affiliation, muc_jid, [attrs]);
} catch (e) {
if (e === null) {
this.alert(__('Timeout error while trying to set the affiliation'), 'danger');

View File

@ -1,5 +1,5 @@
import BaseChatView from 'shared/chat/baseview.js';
import ModeratorToolsModal from 'modals/moderator-tools.js';
import ModeratorToolsModal from './modals/moderator-tools.js';
import log from '@converse/headless/log';
import tpl_muc from './templates/muc.js';
import { Model } from '@converse/skeletor/src/model.js';

View File

@ -1,7 +1,7 @@
import spinner from "templates/spinner.js";
import { __ } from 'i18n';
import { html } from "lit-html";
import { __ } from '../../i18n';
import spinner from "../../templates/spinner.js";
import { modal_header_close_button } from "./buttons.js"
import { modal_header_close_button } from "modals/templates/buttons.js"
function getRoleHelpText (role) {

View File

@ -3107,7 +3107,6 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({
@ -3136,7 +3135,10 @@ describe("Groupchats", function () {
expect(err_msg.textContent.trim()).toBe(
"Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setAffiliation).not.toHaveBeenCalled();
const sel = 'iq[type="set"] query[xmlns="http://jabber.org/protocol/muc#admin"]';
const stanzas = _converse.connection.IQ_stanzas.filter(s => sizzle(sel, s).length);
expect(stanzas.length).toBe(0);
// XXX: Calling onFormSubmitted directly, trying
// again via triggering Event doesn't work for some weird
// reason.
@ -3146,7 +3148,7 @@ describe("Groupchats", function () {
expect(Array.from(view.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe(
"Error: couldn't find a groupchat participant based on your arguments");
expect(view.model.setAffiliation).not.toHaveBeenCalled();
expect(_converse.connection.IQ_stanzas.filter(s => sizzle(sel, s).length).length).toBe(0);
// Call now with the correct of arguments.
// XXX: Calling onFormSubmitted directly, trying
@ -3156,7 +3158,6 @@ describe("Groupchats", function () {
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.setAffiliation).toHaveBeenCalled();
// Check that the member list now gets updated
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3196,7 +3197,6 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({
@ -3224,7 +3224,10 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector('.message:last-child')?.textContent?.trim() ===
"Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setAffiliation).not.toHaveBeenCalled();
const sel = 'iq[type="set"] query[xmlns="http://jabber.org/protocol/muc#admin"]';
const stanzas = _converse.connection.IQ_stanzas.filter(s => sizzle(sel, s).length);
expect(stanzas.length).toBe(0);
// Call now with the correct amount of arguments.
// XXX: Calling onFormSubmitted directly, trying
// again via triggering Event doesn't work for some weird
@ -3233,7 +3236,6 @@ describe("Groupchats", function () {
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setAffiliation).toHaveBeenCalled();
// Check that the member list now gets updated
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+

View File

@ -4,6 +4,7 @@ import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { html } from "lit-html";
import { parseMessageForCommands } from 'plugins/chatview/utils.js';
import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js';
const { Strophe, $pres, $iq, sizzle, u } = converse.env;
@ -187,10 +188,10 @@ function setRole (muc, command, args, required_affiliations = [], required_roles
}
function setAffiliation (muc, command, args, required_affiliations) {
function verifyAndSetAffiliation (muc, command, args, required_affiliations) {
const affiliation = COMMAND_TO_AFFILIATION[command];
if (!affiliation) {
throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
throw Error(`verifyAffiliations called with invalid command: ${command}`);
}
if (!muc.verifyAffiliations(required_affiliations)) {
return false;
@ -223,8 +224,8 @@ function setAffiliation (muc, command, args, required_affiliations) {
if (occupant && api.settings.get('auto_register_muc_nickname')) {
attrs['nick'] = occupant.get('nick');
}
muc
.setAffiliation(affiliation, [attrs])
setAffiliation(affiliation, muc.get('jid'), [attrs])
.then(() => muc.occupants.fetchMembers())
.catch(err => muc.onCommandError(err));
}
@ -249,11 +250,11 @@ export function parseMessageForMUCCommands (muc, text) {
switch (command) {
case 'admin': {
setAffiliation(muc, command, args, ['owner']);
verifyAndSetAffiliation(muc, command, args, ['owner']);
break;
}
case 'ban': {
setAffiliation(muc, command, args, ['admin', 'owner']);
verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
break;
}
case 'modtools': {
@ -293,7 +294,7 @@ export function parseMessageForMUCCommands (muc, text) {
break;
}
case 'member': {
setAffiliation(muc, command, args, ['admin', 'owner']);
verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
break;
}
case 'nick': {
@ -316,7 +317,7 @@ export function parseMessageForMUCCommands (muc, text) {
break;
}
case 'owner':
setAffiliation(muc, command, args, ['owner']);
verifyAndSetAffiliation(muc, command, args, ['owner']);
break;
case 'op': {
setRole(muc, command, args, ['admin', 'owner']);
@ -336,7 +337,7 @@ export function parseMessageForMUCCommands (muc, text) {
break;
}
case 'revoke': {
setAffiliation(muc, command, args, ['admin', 'owner']);
verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
break;
}
case 'topic':