Commit aee6a192 authored by JC Brand's avatar JC Brand

Add a new command `/modtools`

in which you can set user affiliations and roles.

Also, let getAffiliationList return an Error instead of `null` if you're
not allowed to fetch a particular affiliation list.
parent a03e722a
# Changelog # Changelog
## 5.0.1 (Unreleased)
- Add a new GUI for moderator actions. You can trigger it by entering `/modtools` in a MUC.
## 5.0.0 (2019-08-08) ## 5.0.0 (2019-08-08)
- BOSH support has been moved to a plugin. - BOSH support has been moved to a plugin.
- Support for XEP-0410 to check whether we're still present in a room - Support for XEP-0410 to check whether we're still present in a room
- Initial support for the [CredentialsContainer](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer) web API - Initial support for the [CredentialsContainer](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer) web API
......
...@@ -259,7 +259,6 @@ body.converse-fullscreen { ...@@ -259,7 +259,6 @@ body.converse-fullscreen {
input[type=text], input[type=password], input[type=text], input[type=password],
button { button {
font-size: var(--font-size); font-size: var(--font-size);
padding: 0.25em;
min-height: 0; min-height: 0;
} }
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
font-size: var(--font-size); font-size: var(--font-size);
} }
&#converse-register, &#converse-register,
&#converse-login { &#converse-login {
legend { legend {
width: 100%; width: 100%;
...@@ -95,7 +95,6 @@ ...@@ -95,7 +95,6 @@
input[type=submit] { input[type=submit] {
padding-left: 1em; padding-left: 1em;
padding-right: 1em; padding-right: 1em;
margin: 0.5em 0;
border: none; border: none;
} }
input.error { input.error {
......
#conversejs { #conversejs {
#converse-modals { #converse-modals {
.modal-body {
margin-bottom: 2em;
}
.scrollable-container {
max-height: 50vh;
overflow-y: auto;
}
.role-form, .affiliation-form {
padding: 2em 0 1em 0;
}
.set-xmpp-status { .set-xmpp-status {
margin: 1em; margin: 1em;
.custom-control-label { .custom-control-label {
...@@ -43,7 +57,7 @@ ...@@ -43,7 +57,7 @@
width: 100%; width: 100%;
margin-bottom: 1em; margin-bottom: 1em;
} }
.fingerprint-trust { .fingerprint-trust {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
......
(function (root, factory) {
define(["jasmine", "mock", "test-utils" ], factory);
} (this, function (jasmine, mock, test_utils) {
const _ = converse.env._;
const $iq = converse.env.$iq;
const sizzle = converse.env.sizzle;
const Strophe = converse.env.Strophe;
const u = converse.env.utils;
describe("The groupchat moderator tool", function () {
it("allows you to set affiliations and roles",
mock.initConverse(
null, ['rosterGroupsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
let members = [
{'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'},
{'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'},
{'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'},
{'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'owner'},
{'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'},
];
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members);
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => (view.model.occupants.length === 5));
const textarea = view.el.querySelector('.chat-textarea');
textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
view.onKeyDown(enter);
await u.waitUntil(() => view.showModeratorToolsModal.calls.count());
const modal = view.modtools_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000);
let tab = modal.el.querySelector('#affiliations-tab');
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
tab.click();
let select = modal.el.querySelector('.select-affiliation');
expect(select.value).toBe('admin');
let button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]');
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
let user_els = modal.el.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1);
expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: wiccarocks@shakespeare.lit');
expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: wiccan');
expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: admin');
_converse.connection.IQ_stanzas = [];
select.value = 'owner';
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
user_els = modal.el.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(2);
expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit');
expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo');
expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner');
expect(user_els[1].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: crone1@shakespeare.lit');
expect(user_els[1].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: thirdwitch');
expect(user_els[1].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner');
const toggle = user_els[1].querySelector('.list-group-item:nth-child(3n) .toggle-form');
const form = user_els[1].querySelector('.list-group-item:nth-child(3n) .affiliation-form');
expect(u.hasClass('hidden', form)).toBeTruthy();
toggle.click();
expect(u.hasClass('hidden', form)).toBeFalsy();
select = form.querySelector('.select-affiliation');
expect(select.value).toBe('owner');
select.value = 'admin';
const input = form.querySelector('input[name="reason"]');
input.value = "You're an admin now";
const submit = form.querySelector('.btn-primary');
submit.click();
spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough();
const sent_IQ = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${sent_IQ.getAttribute('id')}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item affiliation="admin" jid="crone1@shakespeare.lit">`+
`<reason>You&apos;re an admin now</reason>`+
`</item>`+
`</query>`+
`</iq>`);
_converse.connection.IQ_stanzas = [];
const stanza = $iq({
'type': 'result',
'id': sent_IQ.getAttribute('id'),
'from': view.model.get('jid'),
'to': _converse.connection.jid
});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count());
members = [
{'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'},
{'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'},
{'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'},
{'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'admin'},
{'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'},
];
await test_utils.returnMemberLists(_converse, muc_jid, members);
await u.waitUntil(() => view.model.occupants.pluck('affiliation').filter(o => o === 'owner').length === 1);
const alert = modal.el.querySelector('.alert-primary');
expect(alert.textContent.trim()).toBe('Affiliation changed');
user_els = modal.el.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1);
expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit');
expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo');
expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner');
tab = modal.el.querySelector('#roles-tab');
tab.click();
select = modal.el.querySelector('.select-role');
expect(u.isVisible(select)).toBe(true);
expect(select.value).toBe('moderator');
button = modal.el.querySelector('.btn-primary[name="users_with_role"]');
button.click();
const roles_panel = modal.el.querySelector('#roles-tabpanel');
await u.waitUntil(() => roles_panel.querySelectorAll('.list-group--users > li').length === 1);
select.value = 'participant';
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
user_els = roles_panel.querySelectorAll('.list-group--users > li')
expect(user_els.length).toBe(1);
expect(user_els[0].textContent.trim()).toBe('No users with that role found.');
done();
}));
});
}));
...@@ -1613,7 +1613,13 @@ ...@@ -1613,7 +1613,13 @@
async function (done, _converse) { async function (done, _converse) {
const muc_jid = 'lounge@montague.lit' const muc_jid = 'lounge@montague.lit'
await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', [], ['juliet']);
const members = [{
'nick': 'juliet',
'jid': 'juliet@capulet.lit',
'affiliation': 'member'
}];
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members);
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.model.occupants.length === 2); await u.waitUntil(() => view.model.occupants.length === 2);
...@@ -2975,7 +2981,7 @@ ...@@ -2975,7 +2981,7 @@
view.onKeyDown(enter); view.onKeyDown(enter);
let info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0); let info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_messages.length).toBe(19); expect(info_messages.length).toBe(20);
expect(info_messages.pop().textContent).toBe('/voice: Allow muted user to post messages'); expect(info_messages.pop().textContent).toBe('/voice: Allow muted user to post messages');
expect(info_messages.pop().textContent).toBe('/topic: Set groupchat subject (alias for /subject)'); expect(info_messages.pop().textContent).toBe('/topic: Set groupchat subject (alias for /subject)');
expect(info_messages.pop().textContent).toBe('/subject: Set groupchat subject'); expect(info_messages.pop().textContent).toBe('/subject: Set groupchat subject');
...@@ -2985,6 +2991,7 @@ ...@@ -2985,6 +2991,7 @@
expect(info_messages.pop().textContent).toBe('/op: Grant moderator role to user'); expect(info_messages.pop().textContent).toBe('/op: Grant moderator role to user');
expect(info_messages.pop().textContent).toBe('/nick: Change your nickname'); expect(info_messages.pop().textContent).toBe('/nick: Change your nickname');
expect(info_messages.pop().textContent).toBe('/mute: Remove user\'s ability to post messages'); expect(info_messages.pop().textContent).toBe('/mute: Remove user\'s ability to post messages');
expect(info_messages.pop().textContent).toBe('/modtools: Opens up the moderator tools GUI');
expect(info_messages.pop().textContent).toBe('/member: Grant membership to a user'); expect(info_messages.pop().textContent).toBe('/member: Grant membership to a user');
expect(info_messages.pop().textContent).toBe('/me: Write in 3rd person'); expect(info_messages.pop().textContent).toBe('/me: Write in 3rd person');
expect(info_messages.pop().textContent).toBe('/kick: Kick user from groupchat'); expect(info_messages.pop().textContent).toBe('/kick: Kick user from groupchat');
...@@ -3003,11 +3010,11 @@ ...@@ -3003,11 +3010,11 @@
textarea.value = '/help'; textarea.value = '/help';
view.onKeyDown(enter); view.onKeyDown(enter);
info_messages = sizzle('.chat-info', view.el).slice(1); info_messages = sizzle('.chat-info', view.el).slice(1);
expect(info_messages.length).toBe(17); expect(info_messages.length).toBe(18);
let commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
expect(commands).toEqual([ expect(commands).toEqual([
"/admin", "/ban", "/clear", "/deop", "/destroy", "/admin", "/ban", "/clear", "/deop", "/destroy",
"/help", "/kick", "/me", "/member", "/mute", "/nick", "/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick",
"/op", "/register", "/revoke", "/subject", "/topic", "/voice" "/op", "/register", "/revoke", "/subject", "/topic", "/voice"
]); ]);
occupant.set('affiliation', 'member'); occupant.set('affiliation', 'member');
...@@ -3048,7 +3055,7 @@ ...@@ -3048,7 +3055,7 @@
view.onKeyDown(enter); view.onKeyDown(enter);
const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0); const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
expect(info_messages.length).toBe(17); expect(info_messages.length).toBe(18);
expect(info_messages.pop().textContent).toBe('/topic: Set groupchat subject (alias for /subject)'); expect(info_messages.pop().textContent).toBe('/topic: Set groupchat subject (alias for /subject)');
expect(info_messages.pop().textContent).toBe('/subject: Set groupchat subject'); expect(info_messages.pop().textContent).toBe('/subject: Set groupchat subject');
expect(info_messages.pop().textContent).toBe('/revoke: Revoke the user\'s current affiliation'); expect(info_messages.pop().textContent).toBe('/revoke: Revoke the user\'s current affiliation');
...@@ -3056,6 +3063,7 @@ ...@@ -3056,6 +3063,7 @@
expect(info_messages.pop().textContent).toBe('/owner: Grant ownership of this groupchat'); expect(info_messages.pop().textContent).toBe('/owner: Grant ownership of this groupchat');
expect(info_messages.pop().textContent).toBe('/op: Grant moderator role to user'); expect(info_messages.pop().textContent).toBe('/op: Grant moderator role to user');
expect(info_messages.pop().textContent).toBe('/nick: Change your nickname'); expect(info_messages.pop().textContent).toBe('/nick: Change your nickname');
expect(info_messages.pop().textContent).toBe('/modtools: Opens up the moderator tools GUI');
expect(info_messages.pop().textContent).toBe('/member: Grant membership to a user'); expect(info_messages.pop().textContent).toBe('/member: Grant membership to a user');
expect(info_messages.pop().textContent).toBe('/me: Write in 3rd person'); expect(info_messages.pop().textContent).toBe('/me: Write in 3rd person');
expect(info_messages.pop().textContent).toBe('/kick: Kick user from groupchat'); expect(info_messages.pop().textContent).toBe('/kick: Kick user from groupchat');
...@@ -5366,5 +5374,3 @@ ...@@ -5366,5 +5374,3 @@
}); });
}); });
})); }));
...@@ -16,7 +16,6 @@ import BrowserStorage from "backbone.browserStorage"; ...@@ -16,7 +16,6 @@ import BrowserStorage from "backbone.browserStorage";
import { Overview } from "backbone.overview"; import { Overview } from "backbone.overview";
import bootstrap from "bootstrap.native"; import bootstrap from "bootstrap.native";
import converse from "@converse/headless/converse-core"; import converse from "@converse/headless/converse-core";
import tpl_alert from "templates/alert.html";
import tpl_chatbox from "templates/chatbox.html"; import tpl_chatbox from "templates/chatbox.html";
import tpl_chatbox_head from "templates/chatbox_head.html"; import tpl_chatbox_head from "templates/chatbox_head.html";
import tpl_chatbox_message_form from "templates/chatbox_message_form.html"; import tpl_chatbox_message_form from "templates/chatbox_message_form.html";
...@@ -275,13 +274,7 @@ converse.plugins.add('converse-chatview', { ...@@ -275,13 +274,7 @@ converse.plugins.add('converse-chatview', {
await _converse.api.vcard.update(this.model.contact.vcard, true); await _converse.api.vcard.update(this.model.contact.vcard, true);
} catch (e) { } catch (e) {
_converse.log(e, Strophe.LogLevel.FATAL); _converse.log(e, Strophe.LogLevel.FATAL);
this.el.querySelector('.modal-body').insertAdjacentHTML( this.alert(__('Sorry, something went wrong while trying to refresh'), 'danger');
'afterBegin',
tpl_alert({
'type': 'alert-danger',
'message': __('Sorry, something went wrong while trying to refresh')
})
);
} }
u.removeClass('fa-spin', refresh_icon); u.removeClass('fa-spin', refresh_icon);
}, },
......
...@@ -9,9 +9,10 @@ ...@@ -9,9 +9,10 @@
import "backbone.vdomview"; import "backbone.vdomview";
import bootstrap from "bootstrap.native"; import bootstrap from "bootstrap.native";
import converse from "@converse/headless/converse-core"; import converse from "@converse/headless/converse-core";
import tpl_alert from "templates/alert.html";
import tpl_alert_modal from "templates/alert_modal.html"; import tpl_alert_modal from "templates/alert_modal.html";
const { Strophe, Backbone, _ } = converse.env; const { Strophe, Backbone, sizzle, _ } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -22,6 +23,10 @@ converse.plugins.add('converse-modal', { ...@@ -22,6 +23,10 @@ converse.plugins.add('converse-modal', {
_converse.BootstrapModal = Backbone.VDOMView.extend({ _converse.BootstrapModal = Backbone.VDOMView.extend({
events: {
'click .nav-item .nav-link': 'switchTab'
},
initialize () { initialize () {
this.render().insertIntoDOM(); this.render().insertIntoDOM();
this.modal = new bootstrap.Modal(this.el, { this.modal = new bootstrap.Modal(this.el, {
...@@ -36,6 +41,33 @@ converse.plugins.add('converse-modal', { ...@@ -36,6 +41,33 @@ converse.plugins.add('converse-modal', {
container_el.insertAdjacentElement('beforeEnd', this.el); container_el.insertAdjacentElement('beforeEnd', this.el);
}, },
switchTab (ev) {
ev.stopPropagation();
ev.preventDefault();
sizzle('.nav-link.active', this.el).forEach(el => {
u.removeClass('active', this.el.querySelector(el.getAttribute('href')));
u.removeClass('active', el);
});
u.addClass('active', ev.target);
u.addClass('active', this.el.querySelector(ev.target.getAttribute('href')))
},
alert (message, type='primary') {
const body = this.el.querySelector('.modal-body');
body.insertAdjacentHTML(
'afterBegin',
tpl_alert({
'type': `alert-${type}`,
'message': message
})
);
const el = body.firstElementChild;
setTimeout(() => {
u.addClass('fade-out', el);
setTimeout(() => u.removeElement(el), 600);
}, 5000);
},
show (ev) { show (ev) {
if (ev) { if (ev) {
ev.preventDefault(); ev.preventDefault();
......
...@@ -14,6 +14,7 @@ import "backbone.vdomview"; ...@@ -14,6 +14,7 @@ import "backbone.vdomview";
import BrowserStorage from "backbone.browserStorage"; import BrowserStorage from "backbone.browserStorage";
import { OrderedListView } from "backbone.overview"; import { OrderedListView } from "backbone.overview";
import _FormData from "formdata-polyfill"; import _FormData from "formdata-polyfill";
import bootstrap from "bootstrap.native";
import converse from "@converse/headless/converse-core"; import converse from "@converse/headless/converse-core";
import muc_utils from "@converse/headless/utils/muc"; import muc_utils from "@converse/headless/utils/muc";
import tpl_add_chatroom_modal from "templates/add_chatroom_modal.html"; import tpl_add_chatroom_modal from "templates/add_chatroom_modal.html";
...@@ -32,6 +33,7 @@ import tpl_chatroom_password_form from "templates/chatroom_password_form.html"; ...@@ -32,6 +33,7 @@ import tpl_chatroom_password_form from "templates/chatroom_password_form.html";
import tpl_chatroom_sidebar from "templates/chatroom_sidebar.html"; import tpl_chatroom_sidebar from "templates/chatroom_sidebar.html";
import tpl_info from "templates/info.html"; import tpl_info from "templates/info.html";
import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.html"; import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.html";
import tpl_moderator_tools_modal from "templates/moderator_tools_modal.html";
import tpl_occupant from "templates/occupant.html"; import tpl_occupant from "templates/occupant.html";
import tpl_room_description from "templates/room_description.html"; import tpl_room_description from "templates/room_description.html";
import tpl_room_item from "templates/room_item.html"; import tpl_room_item from "templates/room_item.html";
...@@ -43,8 +45,12 @@ import xss from "xss/dist/xss"; ...@@ -43,8 +45,12 @@ import xss from "xss/dist/xss";
const { Backbone, Promise, Strophe, dayjs, sizzle, _, $iq, $msg, $pres } = converse.env; const { Backbone, Promise, Strophe, dayjs, sizzle, _, $iq, $msg, $pres } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
const ROLES = ['moderator', 'participant', 'visitor'];
const AFFILIATIONS = ['admin', 'member', 'outcast', 'owner'];
const AFFILIATION_CHANGE_COMANDS = ['admin', 'ban', 'owner', 'member', 'revoke'];
const OWNER_COMMANDS = ['owner']; const OWNER_COMMANDS = ['owner'];
const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke']; const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'modtools', 'member', 'op', 'revoke'];
const MODERATOR_COMMANDS = ['kick', 'mute', 'voice']; const MODERATOR_COMMANDS = ['kick', 'mute', 'voice'];
const VISITOR_COMMANDS = ['nick']; const VISITOR_COMMANDS = ['nick'];
...@@ -202,6 +208,167 @@ converse.plugins.add('converse-muc-views', { ...@@ -202,6 +208,167 @@ converse.plugins.add('converse-muc-views', {
} }
_converse.ModeratorToolsModal = _converse.BootstrapModal.extend({
events: {
'submit .affiliation-form': 'assignAffiliation',
'submit .role-form': 'assignRole',
'submit .query-affiliation': 'queryAffiliation',
'submit .query-role': 'queryRole',
'click .nav-item .nav-link': 'switchTab',
'click .toggle-form': 'toggleForm',
},
initialize (attrs) {
this.chatroomview = attrs.chatroomview;
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.model.on('change:role', () => {
this.users_with_role = this.getUsersWithRole();
this.render();
});
this.model.on('change:affiliation', async () => {
this.loading_users_with_affiliation = true;
this.users_with_affiliation = null;
this.render();
const affiliation = this.model.get('affiliation');
if (!_converse.muc_fetch_members || affiliation === 'outcast') {
this.users_with_affiliation = await this.chatroomview.model.getAffiliationList(affiliation);
} else {
this.users_with_affiliation = this.getUsersWithAffiliation();
}
this.loading_users_with_affiliation = false;
this.render();
});
},
toHTML () {
const allowed_commands = this.chatroomview.getAllowedCommands();
const allowed_affiliations = allowed_commands.map(c => COMMAND_TO_AFFILIATION[c]).filter(c => c);
const allowed_roles = _.uniq(allowed_commands
.map(c => COMMAND_TO_ROLE[c])
.filter(c => c));
allowed_affiliations.sort();
allowed_roles.sort();
return tpl_moderator_tools_modal(Object.assign(this.model.toJSON(), {
'__': __,
'affiliations': AFFILIATIONS,
'allowed_affiliations': allowed_affiliations,
'allowed_roles': allowed_roles,
'loading_users_with_affiliation': this.loading_users_with_affiliation,
'roles': ROLES,
'users_with_affiliation': this.users_with_affiliation,
'users_with_role': this.users_with_role
}));
},
toggleForm (ev) {
ev.stopPropagation();
ev.preventDefault();
const form_class = ev.target.getAttribute('data-form');
const form = u.ancestor(ev.target, '.list-group-item').querySelector(`.${form_class}`);
if (u.hasClass('hidden', form)) {
u.removeClass('hidden', form);
} else {
u.addClass('hidden', form);
}
},
getUsersWithAffiliation () {
return this.chatroomview.model.occupants
.where({'affiliation': this.model.get('affiliation')})
.map(item => {
return {
'jid': item.get('jid'),
'nick': item.get('nick'),
'affiliation': item.get('affiliation')
}
});
},
getUsersWithRole () {
return this.chatroomview.model.occupants
.where({'role': this.model.get('role')})
.map(item => {
return {
'jid': item.get('jid'),
'nick': item.get('nick'),
'role': item.get('role')
}
});
},
queryRole (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const role = data.get('role');
this.model.set({'role': null}, {'silent': true});
this.model.set({'role': role});
},
queryAffiliation (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const affiliation = data.get('affiliation');
this.model.set({'affiliation': null}, {'silent': true});
this.model.set({'affiliation': affiliation});
},
assignAffiliation (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const affiliation = data.get('affiliation');
const attrs = {
'jid': data.get('jid'),
'reason': data.get('reason')
}
const current_affiliation = this.model.get('affiliation');
this.chatroomview.model.setAffiliation(affiliation, [attrs])
.then(async () => {
this.alert(__('Affiliation changed'), 'primary');
await this.chatroomview.model.occupants.fetchMembers()
this.model.set({'affiliation': null}, {'silent': true});
this.model.set({'affiliation': current_affiliation});
})
.catch(err => {
this.alert(__('Sorry, something went wrong while trying to set the affiliation'), 'danger');
_converse.log(err, Strophe.LogLevel.ERROR);
});
},
assignRole (ev) {
ev.stopPropagation();
ev.preventDefault();
const data = new FormData(ev.target);
const jid = data.get('jid');
const occupant = this.chatroomview.model.getOccupant(jid);
const role = data.get('role');
const reason = data.get('reason');
const current_role = this.model.get('role');
this.chatroomview.model.setRole(occupant, role, reason,
() => {
this.alert(__('Role changed'), 'primary');
this.model.set({'role': null}, {'silent': true});
this.model.set({'role': current_role});
},
(e) => {
if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
this.alert(__('You\'re not allowed to make that change'), 'danger');
} else {
this.alert(__('Sorry, something went wrong while trying to set the role'), 'danger');
}
_converse.log(e, Strophe.LogLevel.ERROR);
}
);
}
});
_converse.ListChatRoomsModal = _converse.BootstrapModal.extend({ _converse.ListChatRoomsModal = _converse.BootstrapModal.extend({
events: { events: {
...@@ -432,7 +599,6 @@ converse.plugins.add('converse-muc-views', { ...@@ -432,7 +599,6 @@ converse.plugins.add('converse-muc-views', {
/** /**
* The View of an open/ongoing groupchat conversation * The View of an open/ongoing groupchat conversation
*
* @class * @class
* @namespace _converse.ChatRoomView * @namespace _converse.ChatRoomView
* @memberOf _converse * @memberOf _converse
...@@ -594,6 +760,16 @@ converse.plugins.add('converse-muc-views', { ...@@ -594,6 +760,16 @@ converse.plugins.add('converse-muc-views', {
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev); return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
}, },
showModeratorToolsModal (affiliation) {
if (_.isUndefined(this.model.modtools_modal)) {
const model = new Backbone.Model({'affiliation': affiliation});
this.modtools_modal = new _converse.ModeratorToolsModal({'model': model, 'chatroomview': this});
} else {
this.modtools_modal.set('affiliation', affiliation);
}
this.modtools_modal.show();
},
showRoomDetailsModal (ev) { showRoomDetailsModal (ev) {
ev.preventDefault(); ev.preventDefault();
if (this.model.room_details_modal === undefined) { if (this.model.room_details_modal === undefined) {
...@@ -927,6 +1103,26 @@ converse.plugins.add('converse-muc-views', { ...@@ -927,6 +1103,26 @@ converse.plugins.add('converse-muc-views', {
this.showErrorMessage(__("Sorry, an error happened while running the command. Check your browser's developer console for details.")); this.showErrorMessage(__("Sorry, an error happened while running the command. Check your browser's developer console for details."));
}, },
getAllowedCommands () {
// FIXME: The availability of some of these commands
// depend on the MUCs configuration (e.g. whether it's
// moderated or not). We need to take that into
// consideration.
let allowed_commands = ['clear', 'help', 'me', 'nick', 'subject', 'topic', 'register'];
const occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
if (this.verifyAffiliations(['owner'], occupant, false)) {
allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
} else if (this.verifyAffiliations(['admin'], occupant, false)) {
allowed_commands = allowed_commands.concat(ADMIN_COMMANDS);
}
if (this.verifyRoles(['moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS);
} else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(VISITOR_COMMANDS);
}
return allowed_commands;
},
parseMessageForCommands (text) { parseMessageForCommands (text) {
if (_converse.muc_disable_slash_commands && !Array.isArray(_converse.muc_disable_slash_commands)) { if (_converse.muc_disable_slash_commands && !Array.isArray(_converse.muc_disable_slash_commands)) {
return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments); return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
...@@ -936,7 +1132,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -936,7 +1132,7 @@ converse.plugins.add('converse-muc-views', {
if (!command) { if (!command) {
return false; return false;
} }
const args = text.slice(('/'+command).length+1); const args = text.slice(('/'+command).length+1).trim();
let disabled_commands = []; let disabled_commands = [];
if (Array.isArray(_converse.muc_disable_slash_commands)) { if (Array.isArray(_converse.muc_disable_slash_commands)) {
...@@ -955,6 +1151,10 @@ converse.plugins.add('converse-muc-views', { ...@@ -955,6 +1151,10 @@ converse.plugins.add('converse-muc-views', {
this.setAffiliation(command, args, ['admin', 'owner']); this.setAffiliation(command, args, ['admin', 'owner']);
break; break;
} }
case 'modtools': {
this.showModeratorToolsModal(args);
break;
}
case 'deop': { case 'deop': {
// FIXME: /deop only applies to setting a moderators // FIXME: /deop only applies to setting a moderators
// role to "participant" (which only admin/owner can // role to "participant" (which only admin/owner can
...@@ -975,22 +1175,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -975,22 +1175,7 @@ converse.plugins.add('converse-muc-views', {
break; break;
} }
case 'help': { case 'help': {
// FIXME: The availability of some of these commands const allowed_commands = this.getAllowedCommands();
// depend on the MUCs configuration (e.g. whether it's
// moderated or not). We need to take that into
// consideration.
let allowed_commands = ['clear', 'help', 'me', 'nick', 'subject', 'topic', 'register'];
const occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
if (this.verifyAffiliations(['owner'], occupant, false)) {
allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
} else if (this.verifyAffiliations(['admin'], occupant, false)) {
allowed_commands = allowed_commands.concat(ADMIN_COMMANDS);
}
if (this.verifyRoles(['moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS);
} else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(VISITOR_COMMANDS);
}
this.showHelpMessages([`<strong>${__("You can run the following commands")}</strong>`]); this.showHelpMessages([`<strong>${__("You can run the following commands")}</strong>`]);
this.showHelpMessages([ this.showHelpMessages([
`<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`, `<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
...@@ -1002,6 +1187,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -1002,6 +1187,7 @@ converse.plugins.add('converse-muc-views', {
`<strong>/kick</strong>: ${__('Kick user from groupchat')}`, `<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
`<strong>/me</strong>: ${__('Write in 3rd person')}`, `<strong>/me</strong>: ${__('Write in 3rd person')}`,
`<strong>/member</strong>: ${__('Grant membership to a user')}`, `<strong>/member</strong>: ${__('Grant membership to a user')}`,
`<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
`<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`, `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
`<strong>/nick</strong>: ${__('Change your nickname')}`, `<strong>/nick</strong>: ${__('Change your nickname')}`,
`<strong>/op</strong>: ${__('Grant moderator role to user')}`, `<strong>/op</strong>: ${__('Grant moderator role to user')}`,
......
...@@ -1112,10 +1112,11 @@ converse.plugins.add('converse-muc', { ...@@ -1112,10 +1112,11 @@ converse.plugins.add('converse-muc', {
.c("item", {'affiliation': affiliation}); .c("item", {'affiliation': affiliation});
const result = await _converse.api.sendIQ(iq, null, false); const result = await _converse.api.sendIQ(iq, null, false);
if (result.getAttribute('type') === 'error') { if (result.getAttribute('type') === 'error') {
const err_msg = `Not allowed to fetch ${affiliation} list for MUC ${this.get('jid')}`; const err_msg = `Error: not allowed to fetch ${affiliation} list for MUC ${this.get('jid')}`;
const err = new Error(err_msg);
_converse.log(err_msg, Strophe.LogLevel.WARN); _converse.log(err_msg, Strophe.LogLevel.WARN);
_converse.log(result, Strophe.LogLevel.WARN); _converse.log(result, Strophe.LogLevel.WARN);
return null; return err;
} }
return u.parseMemberListIQ(result).filter(p => p); return u.parseMemberListIQ(result).filter(p => p);
}, },
...@@ -1136,8 +1137,8 @@ converse.plugins.add('converse-muc', { ...@@ -1136,8 +1137,8 @@ converse.plugins.add('converse-muc', {
async updateMemberLists (members) { async updateMemberLists (members) {
const all_affiliations = ['member', 'admin', 'owner']; 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 => this.getAffiliationList(a)));
const known_affiliations = all_affiliations.filter(a => aff_lists[all_affiliations.indexOf(a)] !== null); const known_affiliations = all_affiliations.filter(a => !u.isErrorObject(aff_lists[all_affiliations.indexOf(a)]));
const old_members = aff_lists.reduce((acc, val) => (val !== null ? [...val, ...acc] : acc), []); const old_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc: [...val, ...acc]), []);
await this.setAffiliations(u.computeAffiliationsDelta(true, false, members, old_members)); await this.setAffiliations(u.computeAffiliationsDelta(true, false, members, old_members));
if (_converse.muc_fetch_members) { if (_converse.muc_fetch_members) {
return this.occupants.fetchMembers(); return this.occupants.fetchMembers();
...@@ -1911,8 +1912,8 @@ converse.plugins.add('converse-muc', { ...@@ -1911,8 +1912,8 @@ converse.plugins.add('converse-muc', {
async fetchMembers () { async fetchMembers () {
const all_affiliations = ['member', 'admin', 'owner']; const all_affiliations = ['member', 'admin', 'owner'];
const aff_lists = await Promise.all(all_affiliations.map(a => this.chatroom.getAffiliationList(a))); const aff_lists = await Promise.all(all_affiliations.map(a => this.chatroom.getAffiliationList(a)));
const new_members = aff_lists.reduce((acc, val) => (val !== null ? [...val, ...acc] : acc), []); const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
const known_affiliations = all_affiliations.filter(a => aff_lists[all_affiliations.indexOf(a)] !== null); const known_affiliations = all_affiliations.filter(a => !u.isErrorObject(aff_lists[all_affiliations.indexOf(a)]));
const new_jids = new_members.map(m => m.jid).filter(m => m !== undefined); const new_jids = new_members.map(m => m.jid).filter(m => m !== undefined);
const new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => m !== undefined); const new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => m !== undefined);
const removed_members = this.filter(m => { const removed_members = this.filter(m => {
......
...@@ -158,6 +158,10 @@ u.isHeadlineMessage = function (_converse, message) { ...@@ -158,6 +158,10 @@ u.isHeadlineMessage = function (_converse, message) {
return false; return false;
}; };
u.isErrorObject = function (o) {
return o instanceof Error;
}
u.isForbiddenError = function (stanza) { u.isForbiddenError = function (stanza) {
if (!_.isElement(stanza)) { if (!_.isElement(stanza)) {
......
<div class="modal" id="list-chatrooms-modal" tabindex="-1" role="dialog" aria-labelledby="list-chatrooms-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"
id="list-chatrooms-modal-label">{{{o.__('Moderator Tools')}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body d-flex flex-column">
<ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item">
<a class="nav-link active" id="roles-tab" href="#roles-tabpanel" aria-controls="roles-tabpanel" role="tab" data-toggle="tab">Roles</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link" id="affiliations-tab" href="#affiliations-tabpanel" aria-controls="affiliations-tabpanel" role="tab" data-toggle="tab">Affiliations</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="roles-tabpanel" role="tabpanel" aria-labelledby="roles-tab">
<form class="converse-form query-role">
<div class="form-group">
<label for="role">
<strong>{{{o.__('Role')}}}:</strong>
</label>
<div class="row">
<div class="col">
<select class="custom-select select-role" name="role">
{[ o.roles.forEach(function (role) { ]}
<option value="{{{role}}}" {[ if (role === o.role) { ]} selected="selected" {[ } ]}>{{{role}}}</option>
{[ }); ]}
</select>
</div>
<div class="col">
<input type="submit" class="btn btn-primary" name="users_with_role" value="{{{o.__('Show users')}}}"/>
</div>
</div>
</div>
</form>
<div class="scrollable-container">
<ul class="list-group list-group--users">
{[ if (o.loading_users_with_role) { ]}
<li class="list-group-item"> <span class="spinner fa fa-spinner centered"/> </li>
{[ } ]}
{[ if (o.users_with_role && o.users_with_role.length === 0) { ]}
<li class="list-group-item">{{{o.__('No users with that role found.')}}}</li>
{[ } ]}
{[ (o.users_with_role || []).forEach(function (item) { ]}
<li class="list-group-item">
<ul class="list-group">
<li class="list-group-item active">
<div><strong>JID:</strong> {{{item.jid}}}</div>
</li>
<li class="list-group-item">
<div><strong>Nickname:</strong> {{{item.nick}}}</div>
</li>
<li class="list-group-item">
<div><strong>Role:</strong> {{{item.role}}}<a href="#" data-form="role-form" class="toggle-form right fa fa-wrench"></a></div>
<form class="role-form hidden">
<div class="form-group">
<input type="hidden" name="jid" value="{{{item.jid}}}"/>
<input type="hidden" name="nick" value="{{{item.nick}}}"/>
<div class="row">
<div class="col">
<label><strong>{{{o.__('New Role')}}}:</strong></label>
<select class="custom-select select-role" name="role">
{[ o.allowed_roles.forEach(function (role) { ]}
<option value="{{{role}}}" {[ if (role === item.role) { ]} selected="selected" {[ } ]}>{{{role}}}</option>
{[ }); ]}
</select>
</div>
<div class="col">
<label><strong>{{{o.__('Reason')}}}:</strong></label>
<input class="form-control" type="text" name="reason"/>
</div>
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="{{{o.__('Change role')}}}"/>
</div>
</form>
</li>
</ul>
</li>
{[ }); ]}
</ul>
</div>
</div>
<div class="tab-pane" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
<form class="converse-form query-affiliation">
<div class="form-group">
<label for="affiliation">
<strong>{{{o.__('Affiliation')}}}:</strong>
</label>
<div class="row">
<div class="col">
<select class="custom-select select-affiliation" name="affiliation">
{[ o.affiliations.forEach(function (aff) { ]}
<option value="{{{aff}}}" {[ if (aff === o.affiliation) { ]} selected="selected" {[ } ]}>{{{aff}}}</option>
{[ }); ]}
</select>
</div>
<div class="col">
<input type="submit" class="btn btn-primary" name="users_with_affiliation" value="{{{o.__('Show users')}}}"/>
</div>
</div>
</div>
</form>
<ul class="list-group list-group--users">
{[ if (o.loading_users_with_affiliation) { ]}
<li class="list-group-item"> <span class="spinner fa fa-spinner centered"/> </li>
{[ } else { ]}
{[ if (o.users_with_affiliation && o.users_with_affiliation.length === 0) { ]}
<li class="list-group-item">{{{o.__('No users with that affiliation found.')}}}</li>
{[ } ]}
{[ if (o.users_with_affiliation instanceof Error) { ]}
<li class="list-group-item">{{{o.users_with_affiliation.message}}}</li>
{[ } ]}
{[ (o.users_with_affiliation || []).forEach(function (item) { ]}
<li class="list-group-item">
<ul class="list-group">
<li class="list-group-item active">
<div><strong>JID:</strong> {{{item.jid}}}</div>
</li>
<li class="list-group-item">
<div><strong>Nickname:</strong> {{{item.nick}}}</div>
</li>
<li class="list-group-item">
<div><strong>Affiliation:</strong> {{{item.affiliation}}} <a href="#" data-form="affiliation-form" class="toggle-form right fa fa-wrench"></a></div>
<form class="affiliation-form hidden">
<div class="form-group">
<input type="hidden" name="jid" value="{{{item.jid}}}"/>
<input type="hidden" name="nick" value="{{{item.nick}}}"/>
<div class="row">
<div class="col">
<label><strong>{{{o.__('New affiliation')}}}:</strong></label>
<select class="custom-select select-affiliation" name="affiliation">
{[ o.allowed_affiliations.forEach(function (aff) { ]}
<option value="{{{aff}}}" {[ if (aff === item.affiliation) { ]} selected="selected" {[ } ]}>{{{aff}}}</option>
{[ }); ]}
</select>
</div>
<div class="col">
<label><strong>{{{o.__('Reason')}}}:</strong></label>
<input class="form-control" type="text" name="reason"/>
</div>
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" name="change" value="{{{o.__('Change affiliation')}}}"/>
</div>
</form>
</li>
</ul>
</li>
{[ }); ]}
{[ } ]}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
...@@ -55,6 +55,7 @@ var specs = [ ...@@ -55,6 +55,7 @@ var specs = [
"spec/user-details-modal", "spec/user-details-modal",
"spec/messages", "spec/messages",
"spec/muc", "spec/muc",
"spec/modtools",
"spec/room_registration", "spec/room_registration",
"spec/autocomplete", "spec/autocomplete",
"spec/minchats", "spec/minchats",
......
...@@ -213,51 +213,71 @@ ...@@ -213,51 +213,71 @@
}; };
utils.returnMemberLists = async function (_converse, muc_jid, members=[]) { utils.returnMemberLists = async function (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) {
const stanzas = _converse.connection.IQ_stanzas; const stanzas = _converse.connection.IQ_stanzas;
const member_IQ = await u.waitUntil(() => _.filter(
stanzas, if (affiliations.includes('member')) {
s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length const member_IQ = await u.waitUntil(() => _.filter(
).pop()); stanzas,
const member_list_stanza = $iq({ s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length
'from': 'coven@chat.shakespeare.lit', ).pop());
'id': member_IQ.getAttribute('id'), const member_list_stanza = $iq({
'to': 'romeo@montague.lit/orchard', 'from': 'coven@chat.shakespeare.lit',
'type': 'result' 'id': member_IQ.getAttribute('id'),
}).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); 'to': 'romeo@montague.lit/orchard',
members.forEach(member => { 'type': 'result'
member_list_stanza.c('item', { }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
'affiliation': 'member', members.filter(m => m.affiliation === 'member').forEach(m => {
'jid': 'hag66@shakespeare.lit', member_list_stanza.c('item', {
'nick': member, 'affiliation': m.affiliation,
'role': 'participant' 'jid': m.jid,
'nick': m.nick
});
}); });
}); _converse.connection._dataRecv(utils.createRequest(member_list_stanza));
_converse.connection._dataRecv(utils.createRequest(member_list_stanza)); }
const admin_IQ = await u.waitUntil(() => _.filter( if (affiliations.includes('admin')) {
stanzas, const admin_IQ = await u.waitUntil(() => _.filter(
s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length stanzas,
).pop()); s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length
const admin_list_stanza = $iq({ ).pop());
'from': 'coven@chat.shakespeare.lit', const admin_list_stanza = $iq({
'id': admin_IQ.getAttribute('id'), 'from': 'coven@chat.shakespeare.lit',
'to': 'romeo@montague.lit/orchard', 'id': admin_IQ.getAttribute('id'),
'type': 'result' 'to': 'romeo@montague.lit/orchard',
}).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); 'type': 'result'
_converse.connection._dataRecv(utils.createRequest(admin_list_stanza)); }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN});
members.filter(m => m.affiliation === 'admin').forEach(m => {
const owner_IQ = await u.waitUntil(() => _.filter( admin_list_stanza.c('item', {
stanzas, 'affiliation': m.affiliation,
s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length 'jid': m.jid,
).pop()); 'nick': m.nick
const owner_list_stanza = $iq({ });
'from': 'coven@chat.shakespeare.lit', });
'id': owner_IQ.getAttribute('id'), _converse.connection._dataRecv(utils.createRequest(admin_list_stanza));
'to': 'romeo@montague.lit/orchard', }
'type': 'result'
}).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); if (affiliations.includes('owner')) {
_converse.connection._dataRecv(utils.createRequest(owner_list_stanza)); const owner_IQ = await u.waitUntil(() => _.filter(
stanzas,
s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length
).pop());
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});
members.filter(m => m.affiliation === 'owner').forEach(m => {
owner_list_stanza.c('item', {
'affiliation': m.affiliation,
'jid': m.jid,
'nick': m.nick
});
});
_converse.connection._dataRecv(utils.createRequest(owner_list_stanza));
}
}; };
utils.receiveOwnMUCPresence = function (_converse, muc_jid, nick) { utils.receiveOwnMUCPresence = function (_converse, muc_jid, nick) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment