Commit be0274f1 authored by JC Brand's avatar JC Brand

Fixes #1586. Not possible to kick someone with space in nick

Refactored moderation by moving certain methods to the model and
consolidating setting of roles and affiliations into new methods.
parent 5e6c2b99
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
- #1558: `this.get` is not a function error when `forward_messages` is set to `true`. - #1558: `this.get` is not a function error when `forward_messages` is set to `true`.
- #1572: In `fullscreen` view mode the top is cut off on iOS - #1572: In `fullscreen` view mode the top is cut off on iOS
- #1576: Converse gets stuck with spinner when logging out with `auto_login` set to `true` - #1576: Converse gets stuck with spinner when logging out with `auto_login` set to `true`
- #1586: Not possible to kick someone with a space in their nickname
- **Breaking changes**: - **Breaking changes**:
- Rename `muc_disable_moderator_commands` to [muc_disable_slash_commands](https://conversejs.org/docs/html/configuration.html#muc-disable-slash-commands). - Rename `muc_disable_moderator_commands` to [muc_disable_slash_commands](https://conversejs.org/docs/html/configuration.html#muc-disable-slash-commands).
......
...@@ -2802,7 +2802,7 @@ ...@@ -2802,7 +2802,7 @@
expect(_converse.connection.send).not.toHaveBeenCalled(); expect(_converse.connection.send).not.toHaveBeenCalled();
expect(view.el.querySelectorAll('.chat-error').length).toBe(1); expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
expect(view.el.querySelector('.chat-error').textContent.trim()) expect(view.el.querySelector('.chat-error').textContent.trim())
.toBe(`Error: couldn't find a groupchat participant "chris"`) .toBe('Error: couldn\'t find a groupchat participant based on your arguments');
// Now test with an existing nick // Now test with an existing nick
textarea.value = '/member marc Welcome to the club!'; textarea.value = '/member marc Welcome to the club!';
...@@ -2993,7 +2993,7 @@ ...@@ -2993,7 +2993,7 @@
const view = _converse.chatboxviews.get('lounge@localhost'); const view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view.model, 'setAffiliation').and.callThrough(); spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view, 'showErrorMessage').and.callThrough(); spyOn(view, 'showErrorMessage').and.callThrough();
spyOn(view, 'validateRoleChangeCommand').and.callThrough(); spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({ let presence = $pres({
'from': 'lounge@localhost/annoyingGuy', 'from': 'lounge@localhost/annoyingGuy',
...@@ -3015,7 +3015,7 @@ ...@@ -3015,7 +3015,7 @@
preventDefault: _.noop, preventDefault: _.noop,
keyCode: 13 keyCode: 13
}); });
expect(view.validateRoleChangeCommand).toHaveBeenCalled(); expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith( expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason."); "Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setAffiliation).not.toHaveBeenCalled(); expect(view.model.setAffiliation).not.toHaveBeenCalled();
...@@ -3026,7 +3026,7 @@ ...@@ -3026,7 +3026,7 @@
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.showErrorMessage).toHaveBeenCalledWith( expect(view.showErrorMessage).toHaveBeenCalledWith(
'Error: couldn\'t find a groupchat participant "nobody"'); "Error: couldn't find a groupchat participant based on your arguments");
expect(view.model.setAffiliation).not.toHaveBeenCalled(); expect(view.model.setAffiliation).not.toHaveBeenCalled();
// Call now with the correct of arguments. // Call now with the correct of arguments.
...@@ -3036,14 +3036,14 @@ ...@@ -3036,14 +3036,14 @@
textarea.value = '/owner annoyingGuy You\'re responsible'; textarea.value = '/owner annoyingGuy You\'re responsible';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleChangeCommand.calls.count()).toBe(3); expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.setAffiliation).toHaveBeenCalled(); expect(view.model.setAffiliation).toHaveBeenCalled();
expect(view.showErrorMessage.calls.count()).toBe(2); expect(view.showErrorMessage.calls.count()).toBe(2);
// Check that the member list now gets updated // Check that the member list now gets updated
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
`<iq id="${IQ_id}" to="lounge@localhost" type="set" xmlns="jabber:client">`+ `<iq id="${IQ_id}" to="lounge@localhost" type="set" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+ `<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item affiliation="owner" jid="annoyingGuy">`+ `<item affiliation="owner" jid="annoyingguy@localhost">`+
`<reason>You&apos;re responsible</reason>`+ `<reason>You&apos;re responsible</reason>`+
`</item>`+ `</item>`+
`</query>`+ `</query>`+
...@@ -3081,7 +3081,7 @@ ...@@ -3081,7 +3081,7 @@
const view = _converse.chatboxviews.get('lounge@localhost'); const view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view.model, 'setAffiliation').and.callThrough(); spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view, 'showErrorMessage').and.callThrough(); spyOn(view, 'showErrorMessage').and.callThrough();
spyOn(view, 'validateRoleChangeCommand').and.callThrough(); spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({ let presence = $pres({
'from': 'lounge@localhost/annoyingGuy', 'from': 'lounge@localhost/annoyingGuy',
...@@ -3103,7 +3103,7 @@ ...@@ -3103,7 +3103,7 @@
preventDefault: _.noop, preventDefault: _.noop,
keyCode: 13 keyCode: 13
}); });
expect(view.validateRoleChangeCommand).toHaveBeenCalled(); expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith( expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason."); "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setAffiliation).not.toHaveBeenCalled(); expect(view.model.setAffiliation).not.toHaveBeenCalled();
...@@ -3114,14 +3114,14 @@ ...@@ -3114,14 +3114,14 @@
textarea.value = '/ban annoyingGuy You\'re annoying'; textarea.value = '/ban annoyingGuy You\'re annoying';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleChangeCommand.calls.count()).toBe(2); expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.showErrorMessage.calls.count()).toBe(1); expect(view.showErrorMessage.calls.count()).toBe(1);
expect(view.model.setAffiliation).toHaveBeenCalled(); expect(view.model.setAffiliation).toHaveBeenCalled();
// Check that the member list now gets updated // Check that the member list now gets updated
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
`<iq id="${IQ_id}" to="lounge@localhost" type="set" xmlns="jabber:client">`+ `<iq id="${IQ_id}" to="lounge@localhost" type="set" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+ `<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item affiliation="outcast" jid="annoyingGuy">`+ `<item affiliation="outcast" jid="annoyingguy@localhost">`+
`<reason>You&apos;re annoying</reason>`+ `<reason>You&apos;re annoying</reason>`+
`</item>`+ `</item>`+
`</query>`+ `</query>`+
...@@ -3145,7 +3145,7 @@ ...@@ -3145,7 +3145,7 @@
done(); done();
})); }));
it("accepts a /kick command to kick a user", it("takes a /kick command to kick a user",
mock.initConverse( mock.initConverse(
null, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},
async function (done, _converse) { async function (done, _converse) {
...@@ -3161,10 +3161,10 @@ ...@@ -3161,10 +3161,10 @@
const view = _converse.chatboxviews.get('lounge@localhost'); const view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view.model, 'setRole').and.callThrough(); spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'showErrorMessage').and.callThrough(); spyOn(view, 'showErrorMessage').and.callThrough();
spyOn(view, 'validateRoleChangeCommand').and.callThrough(); spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({ let presence = $pres({
'from': 'lounge@localhost/annoyingGuy', 'from': 'lounge@localhost/annoying guy',
'id':'27C55F89-1C6A-459A-9EB5-77690145D624', 'id':'27C55F89-1C6A-459A-9EB5-77690145D624',
'to': 'dummy@localhost/desktop' 'to': 'dummy@localhost/desktop'
}) })
...@@ -3183,7 +3183,7 @@ ...@@ -3183,7 +3183,7 @@
preventDefault: _.noop, preventDefault: _.noop,
keyCode: 13 keyCode: 13
}); });
expect(view.validateRoleChangeCommand).toHaveBeenCalled(); expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith( expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"kick\" command takes two arguments, the user's nickname and optionally a reason."); "Error: the \"kick\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setRole).not.toHaveBeenCalled(); expect(view.model.setRole).not.toHaveBeenCalled();
...@@ -3191,16 +3191,16 @@ ...@@ -3191,16 +3191,16 @@
// XXX: Calling onFormSubmitted directly, trying // XXX: Calling onFormSubmitted directly, trying
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/kick annoyingGuy You\'re annoying'; textarea.value = '/kick annoying guy You\'re annoying';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleChangeCommand.calls.count()).toBe(2); expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.showErrorMessage.calls.count()).toBe(1); expect(view.showErrorMessage.calls.count()).toBe(1);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
`<iq id="${IQ_id}" to="lounge@localhost" type="set" xmlns="jabber:client">`+ `<iq id="${IQ_id}" to="lounge@localhost" type="set" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+ `<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item nick="annoyingGuy" role="none">`+ `<item nick="annoying guy" role="none">`+
`<reason>You&apos;re annoying</reason>`+ `<reason>You&apos;re annoying</reason>`+
`</item>`+ `</item>`+
`</query>`+ `</query>`+
...@@ -3217,7 +3217,7 @@ ...@@ -3217,7 +3217,7 @@
* </presence> * </presence>
*/ */
presence = $pres({ presence = $pres({
'from': 'lounge@localhost/annoyingGuy', 'from': 'lounge@localhost/annoying guy',
'to': 'dummy@localhost/desktop', 'to': 'dummy@localhost/desktop',
'type': 'unavailable' 'type': 'unavailable'
}) })
...@@ -3228,7 +3228,7 @@ ...@@ -3228,7 +3228,7 @@
}).up() }).up()
.c('status', {'code': '307'}); .c('status', {'code': '307'});
_converse.connection._dataRecv(test_utils.createRequest(presence)); _converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.el.querySelectorAll('.chat-info')[3].textContent).toBe("annoyingGuy has been kicked out"); expect(view.el.querySelectorAll('.chat-info')[3].textContent).toBe("annoying guy has been kicked out");
expect(view.el.querySelectorAll('.chat-info').length).toBe(4); expect(view.el.querySelectorAll('.chat-info').length).toBe(4);
done(); done();
})); }));
...@@ -3246,11 +3246,11 @@ ...@@ -3246,11 +3246,11 @@
sent_IQ = iq; sent_IQ = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback); IQ_id = sendIQ.bind(this)(iq, callback, errback);
}); });
var view = _converse.chatboxviews.get('lounge@localhost'); const view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view.model, 'setRole').and.callThrough(); spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'showErrorMessage').and.callThrough(); spyOn(view, 'showErrorMessage').and.callThrough();
spyOn(view, 'showChatEvent').and.callThrough(); spyOn(view, 'showChatEvent').and.callThrough();
spyOn(view, 'validateRoleChangeCommand').and.callThrough(); spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
// New user enters the groupchat // New user enters the groupchat
/* <presence /* <presence
...@@ -3262,7 +3262,7 @@ ...@@ -3262,7 +3262,7 @@
* </x> * </x>
* </presence> * </presence>
*/ */
var presence = $pres({ let presence = $pres({
'from': 'lounge@localhost/trustworthyguy', 'from': 'lounge@localhost/trustworthyguy',
'id':'27C55F89-1C6A-459A-9EB5-77690145D624', 'id':'27C55F89-1C6A-459A-9EB5-77690145D624',
'to': 'dummy@localhost/desktop' 'to': 'dummy@localhost/desktop'
...@@ -3285,7 +3285,7 @@ ...@@ -3285,7 +3285,7 @@
keyCode: 13 keyCode: 13
}); });
expect(view.validateRoleChangeCommand).toHaveBeenCalled(); expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith( expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"op\" command takes two arguments, the user's nickname and optionally a reason."); "Error: the \"op\" command takes two arguments, the user's nickname and optionally a reason.");
...@@ -3297,7 +3297,7 @@ ...@@ -3297,7 +3297,7 @@
textarea.value = '/op trustworthyguy You\'re trustworthy'; textarea.value = '/op trustworthyguy You\'re trustworthy';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleChangeCommand.calls.count()).toBe(2); expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.showErrorMessage.calls.count()).toBe(1); expect(view.showErrorMessage.calls.count()).toBe(1);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
...@@ -3339,7 +3339,7 @@ ...@@ -3339,7 +3339,7 @@
textarea.value = '/deop trustworthyguy Perhaps not'; textarea.value = '/deop trustworthyguy Perhaps not';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleChangeCommand.calls.count()).toBe(3); expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.showChatEvent.calls.count()).toBe(1); expect(view.showChatEvent.calls.count()).toBe(1);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
...@@ -3392,7 +3392,7 @@ ...@@ -3392,7 +3392,7 @@
spyOn(view.model, 'setRole').and.callThrough(); spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'showErrorMessage').and.callThrough(); spyOn(view, 'showErrorMessage').and.callThrough();
spyOn(view, 'showChatEvent').and.callThrough(); spyOn(view, 'showChatEvent').and.callThrough();
spyOn(view, 'validateRoleChangeCommand').and.callThrough(); spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
// New user enters the groupchat // New user enters the groupchat
/* <presence /* <presence
...@@ -3427,7 +3427,7 @@ ...@@ -3427,7 +3427,7 @@
keyCode: 13 keyCode: 13
}); });
expect(view.validateRoleChangeCommand).toHaveBeenCalled(); expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith( expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"mute\" command takes two arguments, the user's nickname and optionally a reason."); "Error: the \"mute\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setRole).not.toHaveBeenCalled(); expect(view.model.setRole).not.toHaveBeenCalled();
...@@ -3438,7 +3438,7 @@ ...@@ -3438,7 +3438,7 @@
textarea.value = '/mute annoyingGuy You\'re annoying'; textarea.value = '/mute annoyingGuy You\'re annoying';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleChangeCommand.calls.count()).toBe(2); expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.showErrorMessage.calls.count()).toBe(1); expect(view.showErrorMessage.calls.count()).toBe(1);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
...@@ -3481,7 +3481,7 @@ ...@@ -3481,7 +3481,7 @@
textarea.value = '/voice annoyingGuy Now you can talk again'; textarea.value = '/voice annoyingGuy Now you can talk again';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
expect(view.validateRoleChangeCommand.calls.count()).toBe(3); expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.showChatEvent.calls.count()).toBe(1); expect(view.showChatEvent.calls.count()).toBe(1);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
......
...@@ -46,6 +46,21 @@ const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revo ...@@ -46,6 +46,21 @@ const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revo
const MODERATOR_COMMANDS = ['kick', 'mute', 'voice']; const MODERATOR_COMMANDS = ['kick', 'mute', 'voice'];
const VISITOR_COMMANDS = ['nick']; const VISITOR_COMMANDS = ['nick'];
const COMMAND_TO_ROLE = {
'deop': 'participant',
'kick': 'none',
'mute': 'visitor',
'op': 'moderator',
'voice': 'participant'
}
const COMMAND_TO_AFFILIATION = {
'admin': 'admin',
'ban': 'outcast',
'member': 'member',
'owner': 'owner',
'revoke': 'none'
}
converse.plugins.add('converse-muc-views', { converse.plugins.add('converse-muc-views', {
/* Dependencies are other plugins which might be /* Dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before * overridden or relied upon, and therefore need to be loaded before
...@@ -96,8 +111,6 @@ converse.plugins.add('converse-muc-views', { ...@@ -96,8 +111,6 @@ converse.plugins.add('converse-muc-views', {
} }
}); });
const OCCUPANT_NOT_FOUND = __("Could not find an occupant with that nickname");
function renderRoomsPanel () { function renderRoomsPanel () {
if (this.roomspanel && u.isVisible(this.roomspanel.el)) { if (this.roomspanel && u.isVisible(this.roomspanel.el)) {
...@@ -857,19 +870,18 @@ converse.plugins.add('converse-muc-views', { ...@@ -857,19 +870,18 @@ converse.plugins.add('converse-muc-views', {
} }
}, },
destroy (groupchat, reason, onSuccess, onError) {
const destroy = $build("destroy");
const iq = $iq({to: groupchat, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_OWNER}).cnode(destroy.node);
if (reason && reason.length > 0) { iq.c("reason", reason); }
return _converse.api.sendIQ(iq);
},
verifyRoles (roles, occupant, show_error=true) { verifyRoles (roles, occupant, show_error=true) {
if (!Array.isArray(roles)) {
throw new TypeError('roles must be an Array');
}
if (!roles.length) {
return true;
}
if (!occupant) { if (!occupant) {
occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid}); occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
} }
const role = occupant.get('role'); const role = occupant.get('role');
if (Array.isArray(roles) && roles.includes(role) || roles === role) { if (roles.includes(role)) {
return true; return true;
} }
if (show_error) { if (show_error) {
...@@ -879,11 +891,17 @@ converse.plugins.add('converse-muc-views', { ...@@ -879,11 +891,17 @@ converse.plugins.add('converse-muc-views', {
}, },
verifyAffiliations (affiliations, occupant, show_error=true) { verifyAffiliations (affiliations, occupant, show_error=true) {
if (!Array.isArray(affiliations)) {
throw new TypeError('affiliations must be an Array');
}
if (!affiliations.length) {
return true;
}
if (!occupant) { if (!occupant) {
occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid}); occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
} }
const a = occupant.get('affiliation'); const a = occupant.get('affiliation');
if (Array.isArray(affiliations) && affiliations.includes(a) || affiliations === a) { if (affiliations.includes(a)) {
return true; return true;
} }
if (show_error) { if (show_error) {
...@@ -892,22 +910,81 @@ converse.plugins.add('converse-muc-views', { ...@@ -892,22 +910,81 @@ converse.plugins.add('converse-muc-views', {
return false; return false;
}, },
validateRoleChangeCommand (command, args) { validateRoleOrAffiliationChangeArgs (command, args) {
/* Check that a command to change a groupchat user's role or if (!args) {
* affiliation has anough arguments.
*/
if (args.length < 1 || args.length > 2) {
this.showErrorMessage( this.showErrorMessage(
__('Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.', command) __('Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.', command)
); );
return false; return false;
} }
if (!(AFFILIATION_CHANGE_COMANDS.includes(command) && u.isValidJID(args[0])) && return true;
!this.model.occupants.findWhere({'nick': args[0]}) && },
!this.model.occupants.findWhere({'jid': args[0]})) {
this.showErrorMessage(__('Error: couldn\'t find a groupchat participant "%1$s"', args[0])); getNickOrJIDFromCommandArgs (args) {
const [text, references] = this.model.parseTextForReferences('@'+args);
if (!references.length) {
this.showErrorMessage(__("Error: couldn't find a groupchat participant based on your arguments"));
return false;
}
return references.pop();
},
setAffiliation (command, args, required_affiliations) {
const affiliation = COMMAND_TO_AFFILIATION[command];
if (!affiliation) {
throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
}
if (!this.verifyAffiliations(required_affiliations)) {
return false;
}
if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = _.get(this.getNickOrJIDFromCommandArgs(args), 'value', null);
if (!nick_or_jid) {
return false;
}
const reason = args.slice(nick_or_jid.length).trim();
// We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
const occupant = this.model.getOccupant(nick_or_jid);
const attrs = {
'jid': occupant.get('jid'),
'reason': reason
}
if (_converse.auto_register_muc_nickname && occupant) {
attrs['nick'] = occupant.get('nick');
}
this.model.setAffiliation(affiliation, [attrs])
.then(() => this.model.occupants.fetchMembers())
.catch(err => this.onCommandError(err));
},
getReason (args) {
return args.includes(',') ? args.slice(args.indexOf(',')+1).trim() : null;
},
setRole (command, args, required_affiliations=[], required_roles=[]) {
/* Check that a command to change a groupchat user's role or
* affiliation has anough arguments.
*/
const role = COMMAND_TO_ROLE[command];
if (!role) {
throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
}
if (!this.verifyAffiliations(required_affiliations) || !this.verifyRoles(required_roles)) {
return false;
}
if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = _.get(this.getNickOrJIDFromCommandArgs(args), 'value', null);
if (!nick_or_jid) {
return false; return false;
} }
const reason = args.slice(nick_or_jid.length).trim();
// We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
const occupant = this.model.getOccupant(nick_or_jid);
this.model.setRole(occupant, role, reason, undefined, this.onCommandError.bind(this));
return true; return true;
}, },
...@@ -920,9 +997,12 @@ converse.plugins.add('converse-muc-views', { ...@@ -920,9 +997,12 @@ converse.plugins.add('converse-muc-views', {
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);
} }
const match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false, '', ''], text = text.replace(/^\s*/, "");
args = match[2] && match[2].splitOnce(' ').filter(s => s) || [], const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
command = match[1].toLowerCase(); if (!command) {
return false;
}
const args = text.slice(('/'+command).length+1);
let disabled_commands = []; let disabled_commands = [];
if (Array.isArray(_converse.muc_disable_slash_commands)) { if (Array.isArray(_converse.muc_disable_slash_commands)) {
...@@ -934,29 +1014,11 @@ converse.plugins.add('converse-muc-views', { ...@@ -934,29 +1014,11 @@ converse.plugins.add('converse-muc-views', {
switch (command) { switch (command) {
case 'admin': { case 'admin': {
if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) { this.setAffiliation(command, args, ['owner']);
break;
}
this.model.setAffiliation('admin', [{
'jid': args[0],
'reason': args[1]
}]).then(
() => this.model.occupants.fetchMembers(),
(err) => this.onCommandError(err)
);
break; break;
} }
case 'ban': { case 'ban': {
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) { this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
this.model.setAffiliation('outcast', [{
'jid': args[0],
'reason': args[1]
}]).then(
() => this.model.occupants.fetchMembers(),
(err) => this.onCommandError(err)
);
break; break;
} }
case 'deop': { case 'deop': {
...@@ -966,24 +1028,14 @@ converse.plugins.add('converse-muc-views', { ...@@ -966,24 +1028,14 @@ converse.plugins.add('converse-muc-views', {
// to participant (e.g. visitor => participant). // to participant (e.g. visitor => participant).
// Currently we don't distinguish between these two // Currently we don't distinguish between these two
// cases. // cases.
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) { this.setRole(command, args, ['admin', 'owner']);
break;
}
const occupant = this.model.getOccupantByNickname(args[0]);
if (!occupant) {
this.showErrorMessage(OCCUPANT_NOT_FOUND);
break;
}
this.model.setRole(
occupant, 'participant', args[1],
undefined, this.onCommandError.bind(this));
break; break;
} }
case 'destroy': { case 'destroy': {
if (!this.verifyAffiliations('owner')) { if (!this.verifyAffiliations(['owner'])) {
break; break;
} }
this.destroy(this.model.get('jid'), args[0]) this.model.sendDestroyIQ(args)
.then(() => this.close()) .then(() => this.close())
.catch(e => this.onCommandError(e)); .catch(e => this.onCommandError(e));
break; break;
...@@ -995,12 +1047,12 @@ converse.plugins.add('converse-muc-views', { ...@@ -995,12 +1047,12 @@ converse.plugins.add('converse-muc-views', {
// consideration. // consideration.
let allowed_commands = ['clear', 'help', 'me', 'nick', 'subject', 'topic', 'register']; let allowed_commands = ['clear', 'help', 'me', 'nick', 'subject', 'topic', 'register'];
const occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid}); const occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
if (this.verifyAffiliations('owner', occupant, false)) { if (this.verifyAffiliations(['owner'], occupant, false)) {
allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS); allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
} else if (this.verifyAffiliations('admin', occupant, false)) { } else if (this.verifyAffiliations(['admin'], occupant, false)) {
allowed_commands = allowed_commands.concat(ADMIN_COMMANDS); allowed_commands = allowed_commands.concat(ADMIN_COMMANDS);
} }
if (this.verifyRoles('moderator', occupant, false)) { if (this.verifyRoles(['moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS); allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS);
} else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) { } else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(VISITOR_COMMANDS); allowed_commands = allowed_commands.concat(VISITOR_COMMANDS);
...@@ -1030,49 +1082,15 @@ converse.plugins.add('converse-muc-views', { ...@@ -1030,49 +1082,15 @@ converse.plugins.add('converse-muc-views', {
); );
break; break;
} case 'kick': { } case 'kick': {
if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) { this.setRole(command, args, [], ['moderator']);
break;
}
const occupant = this.model.getOccupantByNickname(args[0]);
if (!occupant) {
this.showErrorMessage(OCCUPANT_NOT_FOUND);
break;
}
this.model.setRole(
occupant, 'none', args[1],
undefined, this.onCommandError.bind(this));
break; break;
} }
case 'mute': { case 'mute': {
if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) { this.setRole(command, args, [], ['moderator']);
break;
}
const occupant = this.model.getOccupantByNickname(args[0]);
if (!occupant) {
this.showErrorMessage(OCCUPANT_NOT_FOUND);
break;
}
this.model.setRole(
occupant, 'visitor', args[1],
undefined, this.onCommandError.bind(this));
break; break;
} }
case 'member': { case 'member': {
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) { this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
const occupant = this.model.occupants.findWhere({'nick': args[0]}) ||
this.model.occupants.findWhere({'jid': args[0]}),
attrs = {
'jid': occupant ? occupant.get('jid') : args[0],
'reason': args[1]
};
if (_converse.auto_register_muc_nickname && occupant) {
attrs['nick'] = occupant.get('nick');
}
this.model.setAffiliation('member', [attrs])
.then(() => this.model.occupants.fetchMembers())
.catch(err => this.onCommandError(err));
break; break;
} }
case 'nick': { case 'nick': {
...@@ -1081,35 +1099,16 @@ converse.plugins.add('converse-muc-views', { ...@@ -1081,35 +1099,16 @@ converse.plugins.add('converse-muc-views', {
} }
_converse.api.send($pres({ _converse.api.send($pres({
from: _converse.connection.jid, from: _converse.connection.jid,
to: this.model.getRoomJIDAndNick(match[2]), to: this.model.getRoomJIDAndNick(args),
id: _converse.connection.getUniqueId() id: _converse.connection.getUniqueId()
}).tree()); }).tree());
break; break;
} }
case 'owner': case 'owner':
if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) { this.setAffiliation(command, args, ['owner']);
break;
}
this.model.setAffiliation('owner', [{
'jid': args[0],
'reason': args[1]
}]).then(
() => this.model.occupants.fetchMembers(),
(err) => this.onCommandError(err)
);
break; break;
case 'op': { case 'op': {
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) { this.setRole(command, args, ['admin', 'owner']);
break;
}
const occupant = this.model.getOccupantByNickname(args[0]);
if (!occupant) {
this.showErrorMessage();
break;
}
this.model.setRole(
occupant, 'moderator', args[1],
undefined, this.onCommandError.bind(this));
break; break;
} }
case 'register': { case 'register': {
...@@ -1123,42 +1122,15 @@ converse.plugins.add('converse-muc-views', { ...@@ -1123,42 +1122,15 @@ converse.plugins.add('converse-muc-views', {
break; break;
} }
case 'revoke': { case 'revoke': {
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) { this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
this.model.setAffiliation('none', [{
'jid': args[0],
'reason': args[1]
}]).then(
() => this.model.occupants.fetchMembers(),
(err) => this.onCommandError(err)
);
break; break;
} }
case 'topic': case 'topic':
case 'subject': case 'subject':
// TODO: should be done via API call to _converse.api.rooms this.model.setSubject(args);
_converse.api.send(
$msg({
to: this.model.get('jid'),
from: _converse.connection.jid,
type: "groupchat"
}).c("subject", {xmlns: "jabber:client"}).t(match[2] || "").tree()
);
break; break;
case 'voice': { case 'voice': {
if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) { this.setRole(command, args, [], ['moderator']);
break;
}
const occupant = this.model.getOccupantByNickname(args[0]);
if (!occupant) {
this.showErrorMessage(OCCUPANT_NOT_FOUND);
break;
}
this.model.setRole(
occupant, 'participant', args[1], undefined,
this.onCommandError.bind(this)
);
break; break;
} }
default: default:
......
...@@ -361,7 +361,7 @@ converse.plugins.add('converse-muc', { ...@@ -361,7 +361,7 @@ converse.plugins.add('converse-muc', {
* @private * @private
* @method _converse.ChatRoom#join * @method _converse.ChatRoom#join
* @param { String } nick - The user's nickname * @param { String } nick - The user's nickname
* @param { String } password - Optional password, if required by the groupchat. * @param { String } [password] - Optional password, if required by the groupchat.
*/ */
async join (nick, password) { async join (nick, password) {
if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) { if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
...@@ -388,10 +388,36 @@ converse.plugins.add('converse-muc', { ...@@ -388,10 +388,36 @@ converse.plugins.add('converse-muc', {
return this; return this;
}, },
/* Leave the groupchat. /**
* Sends an IQ stanza to the XMPP server to destroy this groupchat. Not
* to be confused with the {@link _converse.ChatRoom#destroy}
* method, which simply removes the room from the local browser storage cache.
* @private
* @method _converse.ChatRoom#sendDestroyIQ
* @param { string } [reason] - The reason for destroying the groupchat
* @param { string } [new_jid] - The JID of the new groupchat which
* replaces this one.
*/
sendDestroyIQ (reason, new_jid) {
const destroy = $build("destroy");
if (new_jid) {
destroy.attrs({'jid': new_jid});
}
const iq = $iq({
'to': this.get('jid'),
'type': "set"
}).c("query", {'xmlns': Strophe.NS.MUC_OWNER}).cnode(destroy.node);
if (reason && reason.length > 0) {
iq.c("reason", reason);
}
return _converse.api.sendIQ(iq);
},
/**
* Leave the groupchat.
* @private * @private
* @method _converse.ChatRoom#leave * @method _converse.ChatRoom#leave
* @param { string } exit_msg - Optional message to indicate your reason for leaving * @param { string } [exit_msg] - Message to indicate your reason for leaving
*/ */
leave (exit_msg) { leave (exit_msg) {
this.features.destroy(); this.features.destroy();
...@@ -550,7 +576,7 @@ converse.plugins.add('converse-muc', { ...@@ -550,7 +576,7 @@ converse.plugins.add('converse-muc', {
* @private * @private
* @method _converse.ChatRoom#directInvite * @method _converse.ChatRoom#directInvite
* @param { String } recipient - JID of the person being invited * @param { String } recipient - JID of the person being invited
* @param { String } reason - Optional reason for the invitation * @param { String } [reason] - Reason for the invitation
*/ */
directInvite (recipient, reason) { directInvite (recipient, reason) {
if (this.features.get('membersonly')) { if (this.features.get('membersonly')) {
...@@ -873,12 +899,14 @@ converse.plugins.add('converse-muc', { ...@@ -873,12 +899,14 @@ converse.plugins.add('converse-muc', {
/** /**
* @private * @private
* @method _converse.ChatRoom#getOccupantByNickname * @method _converse.ChatRoom#getOccupant
* @param { String } nick - The nickname of the occupant to be returned * @param { String } nick_or_jid - The nickname or JID of the occupant to be returned
* @returns { _converse.ChatRoomOccupant } * @returns { _converse.ChatRoomOccupant }
*/ */
getOccupantByNickname (nick) { getOccupant (nick_or_jid) {
return this.occupants.findWhere({'nick': nick}); return (u.isValidJID(nick_or_jid) &&
this.occupants.findWhere({'jid': nick_or_jid})) ||
this.occupants.findWhere({'nick': nick_or_jid});
}, },
async getJidsWithAffiliations (affiliations) { async getJidsWithAffiliations (affiliations) {
...@@ -1146,6 +1174,22 @@ converse.plugins.add('converse-muc', { ...@@ -1146,6 +1174,22 @@ converse.plugins.add('converse-muc', {
return false; return false;
}, },
/**
* Set the subject for this {@link _converse.ChatRoom}
* @private
* @method _converse.ChatRoom#setSubject
* @param { String } value
*/
setSubject(value='') {
_converse.api.send(
$msg({
to: this.get('jid'),
from: _converse.connection.jid,
type: "groupchat"
}).c("subject", {xmlns: "jabber:client"}).t(value).tree()
);
},
/** /**
* Is this a chat state notification that can be ignored, * Is this a chat state notification that can be ignored,
* because it's old or because it's from us. * because it's old or because it's from us.
......
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