Selaa lähdekoodia

In the menu, only show allowed commands

JC Brand 6 vuotta sitten
vanhempi
commit
c72dc74022
5 muutettua tiedostoa jossa 277 lisäystä ja 122 poistoa
  1. 3 2
      .eslintrc.json
  2. 1 0
      CHANGES.md
  3. 146 76
      dist/converse.js
  4. 51 16
      spec/muc.js
  5. 76 28
      src/converse-muc-views.js

+ 3 - 2
.eslintrc.json

@@ -21,8 +21,9 @@
     "rules": {
         "lodash/prefer-lodash-method": [2, {
             "ignoreMethods": [
-                "keys", "find", "endsWith", "startsWith", "filter", "reduce", "isArray", "create",
-                "map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase", "includes"
+                "every", "keys", "find", "endsWith", "startsWith", "filter", "reduce", "isArray",
+                "create", "map", "replace", "some", "toLower", "split", "trim", "forEach",
+                "toUpperCase", "includes"
             ]
         }],
         "lodash/prefer-invoke-map": "off",

+ 1 - 0
CHANGES.md

@@ -7,6 +7,7 @@
 - Take roster nickname into consideration when rendering messages and chat headings.
 - Hide the textarea when a user is muted in a groupchat.
 - Don't restore a BOSH session without knowing the JID
+- In the `/help` menu, only show allowed commands
 - #1296: `embedded` view mode shows `chatbox-navback` arrow in header
 - #1532: Converse reloads on enter pressed in the filter box
 

+ 146 - 76
dist/converse.js

@@ -53468,6 +53468,10 @@ const _converse$env = _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_
       $pres = _converse$env.$pres;
 const u = _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].env.utils;
 const AFFILIATION_CHANGE_COMANDS = ['admin', 'ban', 'owner', 'member', 'revoke'];
+const OWNER_COMMANDS = ['owner'];
+const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke'];
+const MODERATOR_COMMANDS = ['kick', 'mute', 'voice'];
+const VISITOR_COMMANDS = ['nick'];
 _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins.add('converse-muc-views', {
   /* Dependencies are other plugins which might be
    * overridden or relied upon, and therefore need to be loaded before
@@ -54385,26 +54389,40 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
         return _converse.api.sendIQ(iq).then(onSuccess).catch(onError);
       },
 
-      verifyRoles(roles) {
-        const me = this.model.occupants.findWhere({
-          'jid': _converse.bare_jid
-        });
+      verifyRoles(roles, occupant) {
+        let show_error = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
+
+        if (!occupant) {
+          occupant = this.model.occupants.findWhere({
+            'jid': _converse.bare_jid
+          });
+        }
+
+        if (!_.includes(roles, occupant.get('role'))) {
+          if (show_error) {
+            this.showErrorMessage(__('Forbidden: you do not have the necessary role in order to do that.'));
+          }
 
-        if (!_.includes(roles, me.get('role'))) {
-          this.showErrorMessage(__('Forbidden: you do not have the necessary role in order to do that.'));
           return false;
         }
 
         return true;
       },
 
-      verifyAffiliations(affiliations) {
-        const me = this.model.occupants.findWhere({
-          'jid': _converse.bare_jid
-        });
+      verifyAffiliations(affiliations, occupant) {
+        let show_error = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
+
+        if (!occupant) {
+          occupant = this.model.occupants.findWhere({
+            'jid': _converse.bare_jid
+          });
+        }
+
+        if (!_.includes(affiliations, occupant.get('affiliation'))) {
+          if (show_error) {
+            this.showErrorMessage(__('Forbidden: you do not have the necessary affiliation in order to do that.'));
+          }
 
-        if (!_.includes(affiliations, me.get('affiliation'))) {
-          this.showErrorMessage(__('Forbidden: you do not have the necessary affiliation in order to do that.'));
           return false;
         }
 
@@ -54454,62 +54472,104 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
 
         switch (command) {
           case 'admin':
-            if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
+            {
+              if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
+
+              this.model.setAffiliation('admin', [{
+                'jid': args[0],
+                'reason': args[1]
+              }]).then(() => this.model.occupants.fetchMembers(), err => this.onCommandError(err));
               break;
             }
 
-            this.model.setAffiliation('admin', [{
-              'jid': args[0],
-              'reason': args[1]
-            }]).then(() => this.model.occupants.fetchMembers(), err => this.onCommandError(err));
-            break;
-
           case 'ban':
-            if (!this.verifyAffiliations(['owner', 'admin']) || !this.validateRoleChangeCommand(command, args)) {
+            {
+              if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
+
+              this.model.setAffiliation('outcast', [{
+                'jid': args[0],
+                'reason': args[1]
+              }]).then(() => this.model.occupants.fetchMembers(), err => this.onCommandError(err));
               break;
             }
 
-            this.model.setAffiliation('outcast', [{
-              'jid': args[0],
-              'reason': args[1]
-            }]).then(() => this.model.occupants.fetchMembers(), err => this.onCommandError(err));
-            break;
-
           case 'deop':
-            if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
+            {
+              // FIXME: /deop only applies to setting a moderators
+              // role to "participant" (which only admin/owner can
+              // do). Moderators can however set non-moderator's role
+              // to participant (e.g. visitor => participant).
+              // Currently we don't distinguish between these two
+              // cases.
+              if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
+
+              this.modifyRole(this.model.get('jid'), args[0], 'participant', args[1], undefined, this.onCommandError.bind(this));
               break;
             }
 
-            this.modifyRole(this.model.get('jid'), args[0], 'participant', args[1], undefined, this.onCommandError.bind(this));
-            break;
-
           case 'destroy':
-            if (!this.verifyAffiliations(['owner'])) {
+            {
+              if (!this.verifyAffiliations(['owner'])) {
+                break;
+              }
+
+              this.destroy(this.model.get('jid'), args[0]).then(() => this.close()).catch(e => this.onCommandError(e));
               break;
             }
 
-            this.destroy(this.model.get('jid'), args[0]).then(() => this.close()).catch(e => this.onCommandError(e));
-            break;
-
           case 'help':
-            this.showHelpMessages(_.filter([`<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`, `<strong>/ban</strong>: ${__('Ban user from groupchat')}`, `<strong>/clear</strong>: ${__('Remove messages')}`, `<strong>/deop</strong>: ${__('Change user role to participant')}`, `<strong>/destroy</strong>: ${__('Remove this groupchat')}`, `<strong>/help</strong>: ${__('Show this menu')}`, `<strong>/kick</strong>: ${__('Kick user from groupchat')}`, `<strong>/me</strong>: ${__('Write in 3rd person')}`, `<strong>/member</strong>: ${__('Grant membership to a user')}`, `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`, `<strong>/nick</strong>: ${__('Change your nickname')}`, `<strong>/op</strong>: ${__('Grant moderator role to user')}`, `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`, `<strong>/register</strong>: ${__("Register a nickname for this groupchat")}`, `<strong>/revoke</strong>: ${__("Revoke user's membership")}`, `<strong>/subject</strong>: ${__('Set groupchat subject')}`, `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`, `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`], line => _.every(disabled_commands, element => !line.startsWith(element + '<', 9))));
-            break;
+            {
+              // 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
+              });
 
-          case 'kick':
-            if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
+              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>/admin</strong>: ${__("Change user's affiliation to admin")}`, `<strong>/ban</strong>: ${__('Ban user from groupchat')}`, `<strong>/clear</strong>: ${__('Clear the chat area')}`, `<strong>/deop</strong>: ${__('Change user role to participant')}`, `<strong>/destroy</strong>: ${__('Remove this groupchat')}`, `<strong>/help</strong>: ${__('Show this menu')}`, `<strong>/kick</strong>: ${__('Kick user from groupchat')}`, `<strong>/me</strong>: ${__('Write in 3rd person')}`, `<strong>/member</strong>: ${__('Grant membership to a user')}`, `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`, `<strong>/nick</strong>: ${__('Change your nickname')}`, `<strong>/op</strong>: ${__('Grant moderator role to user')}`, `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`, `<strong>/register</strong>: ${__("Register your nickname")}`, `<strong>/revoke</strong>: ${__("Revoke user's membership")}`, `<strong>/subject</strong>: ${__('Set groupchat subject')}`, `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`, `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`].filter(line => disabled_commands.every(c => !line.startsWith(c + '<', 9))).filter(line => allowed_commands.some(c => line.startsWith(c + '<', 9))));
               break;
             }
 
-            this.modifyRole(this.model.get('jid'), args[0], 'none', args[1], undefined, this.onCommandError.bind(this));
-            break;
+          case 'kick':
+            {
+              if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
 
-          case 'mute':
-            if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
+              this.modifyRole(this.model.get('jid'), args[0], 'none', args[1], undefined, this.onCommandError.bind(this));
               break;
             }
 
-            this.modifyRole(this.model.get('jid'), args[0], 'visitor', args[1], undefined, this.onCommandError.bind(this));
-            break;
+          case 'mute':
+            {
+              if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
+
+              this.modifyRole(this.model.get('jid'), args[0], 'visitor', args[1], undefined, this.onCommandError.bind(this));
+              break;
+            }
 
           case 'member':
             {
@@ -54536,17 +54596,19 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
             }
 
           case 'nick':
-            if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
-              break;
-            }
+            {
+              if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
+                break;
+              }
 
-            _converse.api.send($pres({
-              from: _converse.connection.jid,
-              to: this.model.getRoomJIDAndNick(match[2]),
-              id: _converse.connection.getUniqueId()
-            }).tree());
+              _converse.api.send($pres({
+                from: _converse.connection.jid,
+                to: this.model.getRoomJIDAndNick(match[2]),
+                id: _converse.connection.getUniqueId()
+              }).tree());
 
-            break;
+              break;
+            }
 
           case 'owner':
             if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
@@ -54560,35 +54622,41 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
             break;
 
           case 'op':
-            if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
+            {
+              if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
+
+              this.modifyRole(this.model.get('jid'), args[0], 'moderator', args[1], undefined, this.onCommandError.bind(this));
               break;
             }
 
-            this.modifyRole(this.model.get('jid'), args[0], 'moderator', args[1], undefined, this.onCommandError.bind(this));
-            break;
-
           case 'register':
-            if (args.length > 1) {
-              this.showErrorMessage(__('Error: invalid number of arguments'));
-            } else {
-              this.model.registerNickname().then(err_msg => {
-                if (err_msg) this.showErrorMessage(err_msg);
-              });
-            }
+            {
+              if (args.length > 1) {
+                this.showErrorMessage(__('Error: invalid number of arguments'));
+              } else {
+                this.model.registerNickname().then(err_msg => {
+                  if (err_msg) this.showErrorMessage(err_msg);
+                });
+              }
 
-            break;
+              break;
+            }
 
           case 'revoke':
-            if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
+            {
+              if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
+
+              this.model.setAffiliation('none', [{
+                'jid': args[0],
+                'reason': args[1]
+              }]).then(() => this.model.occupants.fetchMembers(), err => this.onCommandError(err));
               break;
             }
 
-            this.model.setAffiliation('none', [{
-              'jid': args[0],
-              'reason': args[1]
-            }]).then(() => this.model.occupants.fetchMembers(), err => this.onCommandError(err));
-            break;
-
           case 'topic':
           case 'subject':
             // TODO: should be done via API call to _converse.api.rooms
@@ -54603,13 +54671,15 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_5__["default"].plugins
             break;
 
           case 'voice':
-            if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
+            {
+              if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
+                break;
+              }
+
+              this.modifyRole(this.model.get('jid'), args[0], 'participant', args[1], undefined, this.onCommandError.bind(this));
               break;
             }
 
-            this.modifyRole(this.model.get('jid'), args[0], 'participant', args[1], undefined, this.onCommandError.bind(this));
-            break;
-
           default:
             return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
         }

+ 51 - 16
spec/muc.js

@@ -1781,9 +1781,7 @@
                     async function (done, _converse) {
 
                 test_utils.createContacts(_converse, 'current'); // We need roster contacts, who can invite us
-                spyOn(window, 'confirm').and.callFake(function () {
-                    return true;
-                });
+                spyOn(window, 'confirm').and.callFake(() => true);
                 await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
                 const view = _converse.chatboxviews.get('lounge@localhost');
                 view.close(); // Hack, otherwise we have to mock stanzas.
@@ -2550,23 +2548,24 @@
                     null, ['rosterGroupsFetched'], {},
                     async function (done, _converse) {
 
+                spyOn(window, 'confirm').and.callFake(() => true);
                 await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy');
                 const view = _converse.chatboxviews.get('lounge@localhost');
-                var textarea = view.el.querySelector('.chat-textarea');
-                textarea.value = '/help This is the groupchat subject';
-                view.keyPressed({
-                    target: textarea,
-                    preventDefault: _.noop,
-                    keyCode: 13
-                });
+                const textarea = view.el.querySelector('.chat-textarea');
+                textarea.value = '/clear';
 
-                const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
+                const enter = { 'target': textarea, 'preventDefault': _.noop, 'keyCode': 13 };
+                view.keyPressed(enter);
+                textarea.value = '/help';
+                view.keyPressed(enter);
+
+                let info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0);
                 expect(info_messages.length).toBe(19);
                 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('/subject: Set groupchat subject');
                 expect(info_messages.pop().textContent).toBe('/revoke: Revoke user\'s membership');
-                expect(info_messages.pop().textContent).toBe('/register: Register a nickname for this groupchat');
+                expect(info_messages.pop().textContent).toBe('/register: Register your nickname');
                 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('/nick: Change your nickname');
@@ -2577,9 +2576,44 @@
                 expect(info_messages.pop().textContent).toBe('/help: Show this menu');
                 expect(info_messages.pop().textContent).toBe('/destroy: Remove this groupchat');
                 expect(info_messages.pop().textContent).toBe('/deop: Change user role to participant');
-                expect(info_messages.pop().textContent).toBe('/clear: Remove messages');
+                expect(info_messages.pop().textContent).toBe('/clear: Clear the chat area');
                 expect(info_messages.pop().textContent).toBe('/ban: Ban user from groupchat');
                 expect(info_messages.pop().textContent).toBe('/admin: Change user\'s affiliation to admin');
+                expect(info_messages.pop().textContent).toBe('You can run the following commands');
+
+                const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});
+                occupant.set('affiliation', 'admin');
+                textarea.value = '/clear';
+                view.keyPressed(enter);
+                textarea.value = '/help';
+                view.keyPressed(enter);
+                info_messages = sizzle('.chat-info', view.el).slice(1);
+                expect(info_messages.length).toBe(17);
+                let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
+                expect(commands).toEqual([
+                    "/admin", "/ban", "/clear", "/deop", "/destroy",
+                    "/help", "/kick", "/me", "/member", "/mute", "/nick",
+                    "/op", "/register", "/revoke", "/subject", "/topic", "/voice"
+                ]);
+                occupant.set('affiliation', 'member');
+                textarea.value = '/clear';
+                view.keyPressed(enter);
+                textarea.value = '/help';
+                view.keyPressed(enter);
+                info_messages = sizzle('.chat-info', view.el).slice(1);
+                expect(info_messages.length).toBe(10);
+                commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
+                expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/mute", "/nick", "/register", "/subject", "/topic", "/voice"]);
+
+                occupant.set('role', 'participant');
+                textarea.value = '/clear';
+                view.keyPressed(enter);
+                textarea.value = '/help';
+                view.keyPressed(enter);
+                info_messages = sizzle('.chat-info', view.el).slice(1);
+                expect(info_messages.length).toBe(7);
+                commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
+                expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register", "/subject", "/topic"]);
                 done();
             }));
 
@@ -2599,11 +2633,11 @@
                 });
 
                 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('/subject: Set groupchat subject');
                 expect(info_messages.pop().textContent).toBe('/revoke: Revoke user\'s membership');
-                expect(info_messages.pop().textContent).toBe('/register: Register a nickname for this groupchat');
+                expect(info_messages.pop().textContent).toBe('/register: Register your nickname');
                 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('/nick: Change your nickname');
@@ -2613,9 +2647,10 @@
                 expect(info_messages.pop().textContent).toBe('/help: Show this menu');
                 expect(info_messages.pop().textContent).toBe('/destroy: Remove this groupchat');
                 expect(info_messages.pop().textContent).toBe('/deop: Change user role to participant');
-                expect(info_messages.pop().textContent).toBe('/clear: Remove messages');
+                expect(info_messages.pop().textContent).toBe('/clear: Clear the chat area');
                 expect(info_messages.pop().textContent).toBe('/ban: Ban user from groupchat');
                 expect(info_messages.pop().textContent).toBe('/admin: Change user\'s affiliation to admin');
+                expect(info_messages.pop().textContent).toBe('You can run the following commands');
                 done();
             }));
 

+ 76 - 28
src/converse-muc-views.js

@@ -41,6 +41,10 @@ import xss from "xss";
 const { Backbone, Promise, Strophe, moment, f, sizzle, _, $build, $iq, $msg, $pres } = converse.env;
 const u = converse.env.utils;
 const AFFILIATION_CHANGE_COMANDS = ['admin', 'ban', 'owner', 'member', 'revoke'];
+const OWNER_COMMANDS = ['owner'];
+const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke'];
+const MODERATOR_COMMANDS = ['kick', 'mute', 'voice'];
+const VISITOR_COMMANDS = ['nick'];
 
 converse.plugins.add('converse-muc-views', {
     /* Dependencies are other plugins which might be
@@ -882,19 +886,27 @@ converse.plugins.add('converse-muc-views', {
                 return _converse.api.sendIQ(iq).then(onSuccess).catch(onError);
             },
 
-            verifyRoles (roles) {
-                const me = this.model.occupants.findWhere({'jid': _converse.bare_jid});
-                if (!_.includes(roles, me.get('role'))) {
-                    this.showErrorMessage(__('Forbidden: you do not have the necessary role in order to do that.'))
+            verifyRoles (roles, occupant, show_error=true) {
+                if (!occupant) {
+                    occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
+                }
+                if (!_.includes(roles, occupant.get('role'))) {
+                    if (show_error) {
+                        this.showErrorMessage(__('Forbidden: you do not have the necessary role in order to do that.'))
+                    }
                     return false;
                 }
                 return true;
             },
 
-            verifyAffiliations (affiliations) {
-                const me = this.model.occupants.findWhere({'jid': _converse.bare_jid});
-                if (!_.includes(affiliations, me.get('affiliation'))) {
-                    this.showErrorMessage(__('Forbidden: you do not have the necessary affiliation in order to do that.'))
+            verifyAffiliations (affiliations, occupant, show_error=true) {
+                if (!occupant) {
+                    occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
+                }
+                if (!_.includes(affiliations, occupant.get('affiliation'))) {
+                    if (show_error) {
+                        this.showErrorMessage(__('Forbidden: you do not have the necessary affiliation in order to do that.'))
+                    }
                     return false;
                 }
                 return true;
@@ -938,7 +950,7 @@ converse.plugins.add('converse-muc-views', {
                     return false;
                 }
                 switch (command) {
-                    case 'admin':
+                    case 'admin': {
                         if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
@@ -950,8 +962,9 @@ converse.plugins.add('converse-muc-views', {
                             (err) => this.onCommandError(err)
                         );
                         break;
-                    case 'ban':
-                        if (!this.verifyAffiliations(['owner', 'admin']) || !this.validateRoleChangeCommand(command, args)) {
+                    }
+                    case 'ban': {
+                        if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
                         this.model.setAffiliation('outcast', [{
@@ -962,15 +975,23 @@ converse.plugins.add('converse-muc-views', {
                             (err) => this.onCommandError(err)
                         );
                         break;
-                    case 'deop':
+                    }
+                    case 'deop': {
+                        // FIXME: /deop only applies to setting a moderators
+                        // role to "participant" (which only admin/owner can
+                        // do). Moderators can however set non-moderator's role
+                        // to participant (e.g. visitor => participant).
+                        // Currently we don't distinguish between these two
+                        // cases.
                         if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
                         this.modifyRole(
-                                this.model.get('jid'), args[0], 'participant', args[1],
-                                undefined, this.onCommandError.bind(this));
+                            this.model.get('jid'), args[0], 'participant', args[1],
+                            undefined, this.onCommandError.bind(this));
                         break;
-                    case 'destroy':
+                    }
+                    case 'destroy': {
                         if (!this.verifyAffiliations(['owner'])) {
                             break;
                         }
@@ -978,11 +999,29 @@ converse.plugins.add('converse-muc-views', {
                             .then(() => this.close())
                             .catch(e => this.onCommandError(e));
                         break;
-                    case 'help':
-                        this.showHelpMessages(_.filter([
+                    }
+                    case 'help': {
+                        // 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);
+                        }
+                        this.showHelpMessages([`<strong>${__("You can run the following commands")}</strong>`]);
+                        this.showHelpMessages([
                             `<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
                             `<strong>/ban</strong>: ${__('Ban user from groupchat')}`,
-                            `<strong>/clear</strong>: ${__('Remove messages')}`,
+                            `<strong>/clear</strong>: ${__('Clear the chat area')}`,
                             `<strong>/deop</strong>: ${__('Change user role to participant')}`,
                             `<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
                             `<strong>/help</strong>: ${__('Show this menu')}`,
@@ -993,15 +1032,16 @@ converse.plugins.add('converse-muc-views', {
                             `<strong>/nick</strong>: ${__('Change your nickname')}`,
                             `<strong>/op</strong>: ${__('Grant moderator role to user')}`,
                             `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
-                            `<strong>/register</strong>: ${__("Register a nickname for this groupchat")}`,
+                            `<strong>/register</strong>: ${__("Register your nickname")}`,
                             `<strong>/revoke</strong>: ${__("Revoke user's membership")}`,
                             `<strong>/subject</strong>: ${__('Set groupchat subject')}`,
                             `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
                             `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
-                        ], line => (_.every(disabled_commands, element => (!line.startsWith(element+'<', 9))))
-                        ));
+                            ].filter(line => disabled_commands.every(c => (!line.startsWith(c+'<', 9))))
+                             .filter(line => allowed_commands.some(c => line.startsWith(c+'<', 9)))
+                        );
                         break;
-                    case 'kick':
+                    } case 'kick': {
                         if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
@@ -1009,7 +1049,8 @@ converse.plugins.add('converse-muc-views', {
                                 this.model.get('jid'), args[0], 'none', args[1],
                                 undefined, this.onCommandError.bind(this));
                         break;
-                    case 'mute':
+                    }
+                    case 'mute': {
                         if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
@@ -1017,6 +1058,7 @@ converse.plugins.add('converse-muc-views', {
                                 this.model.get('jid'), args[0], 'visitor', args[1],
                                 undefined, this.onCommandError.bind(this));
                         break;
+                    }
                     case 'member': {
                         if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
@@ -1034,7 +1076,8 @@ converse.plugins.add('converse-muc-views', {
                             .then(() => this.model.occupants.fetchMembers())
                             .catch(err => this.onCommandError(err));
                         break;
-                    } case 'nick':
+                    }
+                    case 'nick': {
                         if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
                             break;
                         }
@@ -1044,6 +1087,7 @@ converse.plugins.add('converse-muc-views', {
                             id: _converse.connection.getUniqueId()
                         }).tree());
                         break;
+                    }
                     case 'owner':
                         if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
@@ -1056,7 +1100,7 @@ converse.plugins.add('converse-muc-views', {
                             (err) => this.onCommandError(err)
                         );
                         break;
-                    case 'op':
+                    case 'op': {
                         if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
@@ -1064,7 +1108,8 @@ converse.plugins.add('converse-muc-views', {
                                 this.model.get('jid'), args[0], 'moderator', args[1],
                                 undefined, this.onCommandError.bind(this));
                         break;
-                    case 'register':
+                    }
+                    case 'register': {
                         if (args.length > 1) {
                             this.showErrorMessage(__('Error: invalid number of arguments'))
                         } else {
@@ -1073,7 +1118,8 @@ converse.plugins.add('converse-muc-views', {
                             });
                         }
                         break;
-                    case 'revoke':
+                    }
+                    case 'revoke': {
                         if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
@@ -1085,6 +1131,7 @@ converse.plugins.add('converse-muc-views', {
                             (err) => this.onCommandError(err)
                         );
                         break;
+                    }
                     case 'topic':
                     case 'subject':
                         // TODO: should be done via API call to _converse.api.rooms
@@ -1096,7 +1143,7 @@ converse.plugins.add('converse-muc-views', {
                             }).c("subject", {xmlns: "jabber:client"}).t(match[2] || "").tree()
                         );
                         break;
-                    case 'voice':
+                    case 'voice': {
                         if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
                             break;
                         }
@@ -1104,6 +1151,7 @@ converse.plugins.add('converse-muc-views', {
                                 this.model.get('jid'), args[0], 'participant', args[1],
                                 undefined, this.onCommandError.bind(this));
                         break;
+                    }
                     default:
                         return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
                 }