diff --git a/package-lock.json b/package-lock.json
index 9f1c288cd4118bc710250aa0f7e454ce954828d3..7abcd97a9160b6036722103fbf4de323d51ddd7d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1875,8 +1875,8 @@
       }
     },
     "matrix-appservice-bridge": {
-      "version": "github:matrix-org/matrix-appservice-bridge#7abb1228759829500ed7718209b28e197d898dd0",
-      "from": "github:matrix-org/matrix-appservice-bridge#7abb1228759829500ed7718209b28e197d898dd0",
+      "version": "github:matrix-org/matrix-appservice-bridge#8a7288edf1d1d1d1395a83d330d836d9c9bf1e76",
+      "from": "github:matrix-org/matrix-appservice-bridge#8a7288edf1d1d1d1395a83d330d836d9c9bf1e76",
       "requires": {
         "bluebird": "^2.9.34",
         "chalk": "^2.4.1",
@@ -1894,9 +1894,9 @@
       }
     },
     "matrix-js-sdk": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-1.0.4.tgz",
-      "integrity": "sha512-FPx7U1a0SmLbDXhXlR4XHlC+FVKTnK2/+ZBtyOWGLi3nxw4x8hCSSzJ82gzStya1qvhHvbf/y7eblYFVE1l7SQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-1.1.0.tgz",
+      "integrity": "sha512-ECoMN6DkwPdKiMa/jSoMkSDngFCo6x7oH84rLd1NtD7lBPl3Ejj6ARa0iIELE7u0OUO6J0FzdWh7Hd0ZnVTmww==",
       "requires": {
         "another-json": "^0.2.0",
         "babel-runtime": "^6.26.0",
@@ -2456,9 +2456,9 @@
       "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
     },
     "range-parser": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
-      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
     },
     "raw-body": {
       "version": "2.4.0",
diff --git a/src/bot.ts b/src/bot.ts
index a77c32ed0f1714c076dffe06fd19faa7cf69198a..563cc31b98defdb2741267c453bcf800ad5c2c79 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -36,6 +36,7 @@ import { Log } from "./log";
 import * as Discord from "discord.js";
 import * as mime from "mime";
 import { IMatrixEvent, IMatrixMediaInfo } from "./matrixtypes";
+import { DiscordCommandHandler } from "./discordcommandhandler";
 
 const log = new Log("DiscordBot");
 
@@ -74,6 +75,7 @@ export class DiscordBot {
     private channelSync: ChannelSyncroniser;
     private roomHandler: MatrixRoomHandler;
     private provisioner: Provisioner;
+    private discordCommandHandler: DiscordCommandHandler;
     /* Caches */
     private roomIdsForGuildCache: Map<string, {roomIds: string[], ts: number}> = new Map();
 
@@ -99,6 +101,7 @@ export class DiscordBot {
             new MatrixEventProcessorOpts(config, bridge, this),
         );
         this.channelSync = new ChannelSyncroniser(bridge, config, this, store.roomStore);
+        this.discordCommandHandler = new DiscordCommandHandler(bridge, this);
         // init vars
         this.sentMessages = [];
         this.discordMessageQueue = {};
@@ -684,34 +687,9 @@ export class DiscordBot {
             }
         }
 
-        // Check if there's an ongoing bridge request
-        if ((msg.content === "!approve" || msg.content === "!deny") && this.provisioner.HasPendingRequest(chan)) {
-            try {
-                const isApproved = msg.content === "!approve";
-                const successfullyBridged = await this.provisioner.MarkApproved(chan, msg.member, isApproved);
-                if (successfullyBridged && isApproved) {
-                    await msg.channel.sendMessage("Thanks for your response! The matrix bridge has been approved");
-                } else if (successfullyBridged && !isApproved) {
-                    await msg.channel.sendMessage("Thanks for your response! The matrix bridge has been declined");
-                } else {
-                    await msg.channel.sendMessage("Thanks for your response, however" +
-                        "the time for responses has expired - sorry!");
-                }
-            } catch (err) {
-                if (err.message === "You do not have permission to manage webhooks in this channel") {
-                    await msg.channel.sendMessage(err.message);
-                } else {
-                    log.error("Error processing room approval");
-                    log.error(err);
-                }
-            }
-
-            return; // stop processing - we're approving/declining the bridge request
-        }
-
         // check if it is a command to process by the bot itself
         if (msg.content.startsWith("!matrix")) {
-            await this.roomHandler.HandleDiscordCommand(msg);
+            await this.discordCommandHandler.Process(msg);
             return;
         }
 
diff --git a/src/discordcommandhandler.ts b/src/discordcommandhandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f3804c640c82665c946dfd64adf5a0642ae7b2d
--- /dev/null
+++ b/src/discordcommandhandler.ts
@@ -0,0 +1,134 @@
+/*
+Copyright 2019 matrix-appservice-discord
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { DiscordBot } from "./bot";
+import * as Discord from "discord.js";
+import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util";
+import { Bridge } from "matrix-appservice-bridge";
+export class DiscordCommandHandler {
+    constructor(
+        private bridge: Bridge,
+        private discord: DiscordBot,
+    ) { }
+
+    public async Process(msg: Discord.Message) {
+        const chan = msg.channel as Discord.TextChannel;
+        if (!chan.guild) {
+            await msg.channel.send("**ERROR:** only available for guild channels");
+            return;
+        }
+
+        const intent = this.bridge.getIntent();
+
+        const actions: ICommandActions = {
+            approve: {
+                description: "Approve a pending bridge request",
+                params: [],
+                permission: "MANAGE_WEBHOOKS",
+                run: async () => {
+                    if (await this.discord.Provisioner.MarkApproved(chan, msg.member, true)) {
+                        return "Thanks for your response! The matrix bridge has been approved";
+                    } else {
+                        return "Thanks for your response, however" +
+                            "the time for responses has expired - sorry!";
+                    }
+                },
+            },
+            ban: {
+                description: "Bans a user on the matrix side",
+                params: ["name"],
+                permission: "BAN_MEMBERS",
+                run: this.ModerationActionGenerator(chan, "ban"),
+            },
+            deny: {
+                description: "Deny a pending bridge request",
+                params: [],
+                permission: "MANAGE_WEBHOOKS",
+                run: async () => {
+                    if (await this.discord.Provisioner.MarkApproved(chan, msg.member, false)) {
+                        return "Thanks for your response! The matrix bridge has been declined";
+                    } else {
+                        return "Thanks for your response, however" +
+                            "the time for responses has expired - sorry!";
+                    }
+                },
+            },
+            kick: {
+                description: "Kicks a user on the matrix side",
+                params: ["name"],
+                permission: "KICK_MEMBERS",
+                run: this.ModerationActionGenerator(chan, "kick"),
+            },
+            unban: {
+                description: "Unbans a user on the matrix side",
+                params: ["name"],
+                permission: "BAN_MEMBERS",
+                run: this.ModerationActionGenerator(chan, "unban"),
+            },
+        };
+
+        const parameters: ICommandParameters = {
+            name: {
+                description: "The display name or mxid of a matrix user",
+                get: async (name) => {
+                    const channelMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(msg.channel);
+                    const mxUserId = await Util.GetMxidFromName(intent, name, channelMxids);
+                    return mxUserId;
+                },
+            },
+        };
+
+        const permissionCheck: CommandPermissonCheck = async (permission) => {
+            return msg.member.hasPermission(permission as Discord.PermissionResolvable);
+        };
+
+        const reply = await Util.ParseCommand("!matrix", msg.content, actions, parameters, permissionCheck);
+        await msg.channel.send(reply);
+    }
+
+    private ModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: "kick"|"ban"|"unban") {
+        return async ({name}) => {
+            let allChannelMxids: string[] = [];
+            await Promise.all(discordChannel.guild.channels.map(async (chan) => {
+                try {
+                    const chanMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(chan);
+                    allChannelMxids = allChannelMxids.concat(chanMxids);
+                } catch (e) {
+                    // pass, non-text-channel
+                }
+            }));
+            let errorMsg = "";
+            await Promise.all(allChannelMxids.map(async (chanMxid) => {
+                const intent = this.bridge.getIntent();
+                try {
+                    await intent[funcKey](chanMxid, name);
+                } catch (e) {
+                    // maybe we don't have permission to kick/ban/unban...?
+                    errorMsg += `\nCouldn't ${funcKey} ${name} from ${chanMxid}`;
+                }
+            }));
+            if (errorMsg) {
+                throw Error(errorMsg);
+            }
+            const action = {
+                ban: "Banned",
+                kick: "Kicked",
+                unban: "Unbanned",
+            }[funcKey];
+            return `${action} ${name}`;
+        };
+    }
+}
diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts
index a116d6f5f127220f2cbb347e5a6906a024b4e1ce..b9e00a9cbfa7237d11361aa19c512b761105bd3c 100644
--- a/src/matrixcommandhandler.ts
+++ b/src/matrixcommandhandler.ts
@@ -1,11 +1,28 @@
+/*
+Copyright 2019 matrix-appservice-discord
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
 import { DiscordBot } from "./bot";
 import { Log } from "./log";
 import { DiscordBridgeConfig } from "./config";
 import { Bridge, BridgeContext } from "matrix-appservice-bridge";
 import { IMatrixEvent } from "./matrixtypes";
 import { Provisioner } from "./provisioner";
-import { Util } from "./util";
+import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util";
 import * as Discord from "discord.js";
+import * as markdown from "discord-markdown";
 const log = new Log("MatrixCommandHandler");
 
 /* tslint:disable:no-magic-numbers */
@@ -35,175 +52,148 @@ export class MatrixCommandHandler {
         }
     }
 
-    public async ProcessCommand(event: IMatrixEvent, context: BridgeContext) {
+    public async Process(event: IMatrixEvent, context: BridgeContext) {
         const intent = this.bridge.getIntent();
         if (!(await this.isBotInRoom(event.room_id))) {
             log.warn(`Bot is not in ${event.room_id}. Ignoring command`);
             return;
         }
 
-        if (!this.config.bridge.enableSelfServiceBridging) {
-            // We can do this here because the only commands we support are self-service bridging
-            return this.bridge.getIntent().sendMessage(event.room_id, {
-                body: "The owner of this bridge does not permit self-service bridging.",
-                msgtype: "m.notice",
-            });
-        }
-
-        // Check to make sure the user has permission to do anything in the room. We can do this here
-        // because the only commands we support are self-service commands (which therefore require some
-        // level of permissions)
-        const plEvent = await this.bridge.getIntent().getClient()
-            .getStateEvent(event.room_id, "m.room.power_levels", "");
-        let userLevel = PROVISIONING_DEFAULT_USER_POWER_LEVEL;
-        let requiredLevel = PROVISIONING_DEFAULT_POWER_LEVEL;
-        if (plEvent && plEvent.state_default) {
-            requiredLevel = plEvent.state_default;
-        }
-        if (plEvent && plEvent.users_default) {
-            userLevel = plEvent.users_default;
-        }
-        if (plEvent && plEvent.users && plEvent.users[event.sender]) {
-            userLevel = plEvent.users[event.sender];
-        }
-
-        if (userLevel < requiredLevel) {
-            return this.bridge.getIntent().sendMessage(event.room_id, {
-                body: "You do not have the required power level in this room to create a bridge to a Discord channel.",
-                msgtype: "m.notice",
-            });
-        }
-
-        const {command, args} = Util.MsgToArgs(event.content!.body as string, "!discord");
-
-        if (command === "help" && args[0] === "bridge") {
-            const link = Util.GetBotLink(this.config);
-            // tslint:disable prefer-template
-            return this.bridge.getIntent().sendMessage(event.room_id, {
-                body: "How to bridge a Discord guild:\n" +
-                "1. Invite the bot to your Discord guild using this link: " + link + "\n" +
-                "2. Invite me to the matrix room you'd like to bridge\n" +
-                "3. Open the Discord channel you'd like to bridge in a web browser\n" +
-                "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " +
-                "(without the backticks)\n" +
-                "   Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" +
-                "   The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" +
-                "5. Enjoy your new bridge!",
-                msgtype: "m.notice",
-            });
-            // tslint:enable prefer-template
-        } else if (command === "bridge") {
-            if (context.rooms.remote) {
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "This room is already bridged to a Discord guild.",
-                    msgtype: "m.notice",
-                });
-            }
-
-            const MAXARGS = 2;
-            if (args.length > MAXARGS || args.length < 1) {
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "Invalid syntax. For more information try !discord help bridge",
-                    msgtype: "m.notice",
-                });
-            }
-
-            let guildId: string;
-            let channelId: string;
-
-            const AMOUNT_OF_IDS_DISCORD_IDENTIFIES_ROOMS_BY = 2;
-
-            if (args.length === AMOUNT_OF_IDS_DISCORD_IDENTIFIES_ROOMS_BY) { // "x y" syntax
-                guildId = args[0];
-                channelId = args[1];
-            } else if (args.length === 1 && args[0].includes("/")) { // "x/y" syntax
-                const split = args[0].split("/");
-                guildId = split[0];
-                channelId = split[1];
-            } else {
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "Invalid syntax: See `!discord help`",
-                    formatted_body: "Invalid syntax: See <code>!discord help</code>",
-                    msgtype: "m.notice",
-                });
-            }
-
-            try {
-                const discordResult = await this.discord.LookupRoom(guildId, channelId);
-                const channel = discordResult.channel as Discord.TextChannel;
-
-                log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`);
-                this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "I'm asking permission from the guild administrators to make this bridge.",
-                    msgtype: "m.notice",
-                });
-
-                await this.provisioner.AskBridgePermission(channel, event.sender);
-                await this.provisioner.BridgeMatrixRoom(channel, event.room_id);
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "I have bridged this room to your channel",
-                    msgtype: "m.notice",
-                });
-            } catch (err) {
-                if (err.message === "Timed out waiting for a response from the Discord owners"
-                    || err.message === "The bridge has been declined by the Discord guild") {
-                    return this.bridge.getIntent().sendMessage(event.room_id, {
-                        body: err.message,
-                        msgtype: "m.notice",
-                    });
-                }
-
-                log.error(`Error bridging ${event.room_id} to ${guildId}/${channelId}`);
-                log.error(err);
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "There was a problem bridging that channel - has the guild owner approved the bridge?",
-                    msgtype: "m.notice",
-                });
-            }
-        } else if (command === "unbridge") {
-            const remoteRoom = context.rooms.remote;
-
-            if (!remoteRoom) {
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "This room is not bridged.",
-                    msgtype: "m.notice",
-                });
+        const actions: ICommandActions = {
+            bridge: {
+                description: "Bridges this room to a Discord channel",
+                // tslint:disable prefer-template
+                help: "How to bridge a Discord guild:\n" +
+                    "1. Invite the bot to your Discord guild using this link: " + Util.GetBotLink(this.config) + "\n" +
+                    "2. Invite me to the matrix room you'd like to bridge\n" +
+                    "3. Open the Discord channel you'd like to bridge in a web browser\n" +
+                    "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " +
+                    "(without the backticks)\n" +
+                    "   Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" +
+                    "   The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" +
+                    "5. Enjoy your new bridge!",
+                // tslint:enable prefer-template
+                params: ["guildId", "channelId"],
+                permission: {
+                    cat: "events",
+                    level: PROVISIONING_DEFAULT_POWER_LEVEL,
+                    selfService: true,
+                    subcat: "m.room.power_levels",
+                },
+                run: async ({guildId, channelId}) => {
+                    if (context.rooms.remote) {
+                        return "This room is already bridged to a Discord guild.";
+                    }
+                    if (!guildId || !channelId) {
+                        return "Invalid syntax. For more information try `!discord help bridge`";
+                    }
+                    try {
+                        const discordResult = await this.discord.LookupRoom(guildId, channelId);
+                        const channel = discordResult.channel as Discord.TextChannel;
+
+                        log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`);
+                        this.bridge.getIntent().sendMessage(event.room_id, {
+                            body: "I'm asking permission from the guild administrators to make this bridge.",
+                            msgtype: "m.notice",
+                        });
+
+                        await this.provisioner.AskBridgePermission(channel, event.sender);
+                        await this.provisioner.BridgeMatrixRoom(channel, event.room_id);
+                        return "I have bridged this room to your channel";
+                    } catch (err) {
+                        if (err.message === "Timed out waiting for a response from the Discord owners"
+                            || err.message === "The bridge has been declined by the Discord guild") {
+                            return err.message;
+                        }
+
+                        log.error(`Error bridging ${event.room_id} to ${guildId}/${channelId}`);
+                        log.error(err);
+                        return "There was a problem bridging that channel - has the guild owner approved the bridge?";
+                    }
+                },
+            },
+            unbridge: {
+                description: "Unbridges a Discord channel from this room",
+                params: [],
+                permission: {
+                    cat: "events",
+                    level: PROVISIONING_DEFAULT_POWER_LEVEL,
+                    selfService: true,
+                    subcat: "m.room.power_levels",
+                },
+                run: async () => {
+                    const remoteRoom = context.rooms.remote;
+                    if (!remoteRoom) {
+                        return "This room is not bridged.";
+                    }
+                    if (!remoteRoom.data.plumbed) {
+                        return "This room cannot be unbridged.";
+                    }
+                    try {
+                        await this.provisioner.UnbridgeRoom(remoteRoom);
+                        return "This room has been unbridged";
+                    } catch (err) {
+                        log.error("Error while unbridging room " + event.room_id);
+                        log.error(err);
+                        return "There was an error unbridging this room. " +
+                            "Please try again later or contact the bridge operator.";
+                    }
+                },
+            },
+        };
+
+        /*
+        We hack together that "guildId/channelId" is the same as "guildId channelId".
+        We do this by assuming that guildId is parsed first, and split at "/"
+        The first element is returned, the second one is passed on to channelId, if applicable.
+        */
+        let guildIdRemainder: string | undefined;
+        const parameters: ICommandParameters = {
+            channelId: {
+                description: "The ID of a channel on discord",
+                get: async (s) => {
+                    if (!s && guildIdRemainder) {
+                        return guildIdRemainder;
+                    }
+                    return s;
+                },
+            },
+            guildId: {
+                description: "The ID of a guild/server on discord",
+                get: async (s) => {
+                    if (!s) {
+                        return s;
+                    }
+                    const parts = s.split("/");
+                    guildIdRemainder = parts[1];
+                    return parts[0];
+                },
+            },
+        };
+
+        const permissionCheck: CommandPermissonCheck = async (permission) => {
+            if (permission.selfService && !this.config.bridge.enableSelfServiceBridging) {
+                return "The owner of this bridge does not permit self-service bridging.";
             }
-
-            if (!remoteRoom.data.plumbed) {
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "This room cannot be unbridged.",
-                    msgtype: "m.notice",
-                });
-            }
-
-            try {
-                await this.provisioner.UnbridgeRoom(remoteRoom);
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "This room has been unbridged",
-                    msgtype: "m.notice",
-                });
-            } catch (err) {
-                log.error("Error while unbridging room " + event.room_id);
-                log.error(err);
-                return this.bridge.getIntent().sendMessage(event.room_id, {
-                    body: "There was an error unbridging this room. " +
-                      "Please try again later or contact the bridge operator.",
-                    msgtype: "m.notice",
-                });
-            }
-        } else if (command === "help") {
-            // Unknown command or no command given to get help on, so we'll just give them the help
-            // tslint:disable prefer-template
-            return this.bridge.getIntent().sendMessage(event.room_id, {
-                body: "Available commands:\n" +
-                "!discord bridge <guild id> <channel id>   - Bridges this room to a Discord channel\n" +
-                "!discord unbridge                         - Unbridges a Discord channel from this room\n" +
-                "!discord help <command>                   - Help menu for another command. Eg: !discord help bridge\n",
-                msgtype: "m.notice",
-            });
-            // tslint:enable prefer-template
-        }
+            return await Util.CheckMatrixPermission(
+                this.bridge.getIntent().getClient(),
+                event.sender,
+                event.room_id,
+                permission.level,
+                permission.cat,
+                permission.subcat,
+            );
+        };
+
+        const reply = await Util.ParseCommand("!discord", event.content!.body!, actions, parameters, permissionCheck);
+        const formattedReply = markdown.toHTML(reply);
+
+        await this.bridge.getIntent().sendMessage(event.room_id, {
+            body: reply,
+            format: "org.matrix.custom.html",
+            formatted_body: formattedReply,
+            msgtype: "m.notice",
+        });
     }
 
     private async isBotInRoom(roomId: string): Promise<boolean> {
diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts
index 28945add25b48c88d2916a52682cc2e429a99d63..7c7f6a516ccfd7114329f177280c2429eefb76ce 100644
--- a/src/matrixeventprocessor.ts
+++ b/src/matrixeventprocessor.ts
@@ -115,7 +115,7 @@ export class MatrixEventProcessor {
                 event.content!.body &&
                 event.content!.body!.startsWith("!discord");
             if (isBotCommand) {
-                await this.mxCommandHandler.ProcessCommand(event, context);
+                await this.mxCommandHandler.Process(event, context);
                 return;
             } else if (context.rooms.remote) {
                 const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS);
diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts
index 124978d7689fb22620c6ac60d9bf76e8d7217b70..c455aac924af09f6cd5f94f1aecfab1df512202c 100644
--- a/src/matrixmessageprocessor.ts
+++ b/src/matrixmessageprocessor.ts
@@ -80,19 +80,14 @@ export class MatrixMessageProcessor {
         if (!this.params || !this.params.mxClient || !this.params.roomId || !this.params.userId) {
             return false;
         }
-
-        const res: IMatrixEvent = await this.params.mxClient.getStateEvent(
-            this.params.roomId, "m.room.power_levels");
-
-        // Some rooms may not have notifications.room set if the value hasn't
-        // been changed from the default. If so, use our hardcoded power level.
-        const requiredPowerLevel = res && res.notifications && res.notifications.room
-            ? res.notifications.room
-            : DEFAULT_ROOM_NOTIFY_POWER_LEVEL;
-
-        return res && res.users
-            && res.users[this.params.userId] !== undefined
-            && res.users[this.params.userId] >= requiredPowerLevel;
+        return await Util.CheckMatrixPermission(
+            this.params.mxClient,
+            this.params.userId,
+            this.params.roomId,
+            DEFAULT_ROOM_NOTIFY_POWER_LEVEL,
+            "notifications",
+            "room",
+        );
     }
 
     private async escapeDiscord(msg: string): Promise<string> {
diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts
index c648f6e503884cd2395bf076b76a375959c72fd9..a91d4c76eb22b7e366a73a8e8ee093f76d85783e 100644
--- a/src/matrixroomhandler.ts
+++ b/src/matrixroomhandler.ts
@@ -28,7 +28,7 @@ import {
 import { DiscordBridgeConfig } from "./config";
 
 import * as Discord from "discord.js";
-import { Util, ICommandActions, ICommandParameters } from "./util";
+import { Util } from "./util";
 import { Provisioner } from "./provisioner";
 import { Log } from "./log";
 const log = new Log("MatrixRoomHandler");
@@ -218,117 +218,6 @@ export class MatrixRoomHandler {
         throw {err: "Unsupported", code: HTTP_UNSUPPORTED};
     }
 
-    public async HandleDiscordCommand(msg: Discord.Message) {
-        if (!(msg.channel as Discord.TextChannel).guild) {
-            await msg.channel.send("**ERROR:** only available for guild channels");
-        }
-
-        const {command, args} = Util.MsgToArgs(msg.content, "!matrix");
-
-        const intent = this.bridge.getIntent();
-
-        const actions: ICommandActions = {
-            ban: {
-                description: "Bans a user on the matrix side",
-                params: ["name"],
-                permission: "BAN_MEMBERS",
-                run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "ban", "Banned"),
-            },
-            kick: {
-                description: "Kicks a user on the matrix side",
-                params: ["name"],
-                permission: "KICK_MEMBERS",
-                run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "kick", "Kicked"),
-            },
-            unban: {
-                description: "Unbans a user on the matrix side",
-                params: ["name"],
-                permission: "BAN_MEMBERS",
-                run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "unban", "Unbanned"),
-            },
-        };
-
-        const parameters: ICommandParameters = {
-            name: {
-                description: "The display name or mxid of a matrix user",
-                get: async (name) => {
-                    const channelMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(msg.channel);
-                    const mxUserId = await Util.GetMxidFromName(intent, name, channelMxids);
-                    return mxUserId;
-                },
-            },
-        };
-
-        if (command === "help") {
-            let replyHelpMessage = "Available Commands:\n";
-            for (const actionKey of Object.keys(actions)) {
-                const action = actions[actionKey];
-                if (!msg.member.hasPermission(action.permission as Discord.PermissionResolvable)) {
-                    continue;
-                }
-                replyHelpMessage += " - `!matrix " + actionKey;
-                for (const param of action.params) {
-                    replyHelpMessage += ` <${param}>`;
-                }
-                replyHelpMessage += `\`: ${action.description}\n`;
-            }
-            replyHelpMessage += "\nParameters:\n";
-            for (const parameterKey of Object.keys(parameters)) {
-                const parameter = parameters[parameterKey];
-                replyHelpMessage += ` - \`<${parameterKey}>\`: ${parameter.description}\n`;
-            }
-            await msg.channel.send(replyHelpMessage);
-            return;
-        }
-
-        if (!actions[command]) {
-            await msg.channel.send("**Error:** unknown command. Try `!matrix help` to see all commands");
-            return;
-        }
-
-        if (!msg.member.hasPermission(actions[command].permission as Discord.PermissionResolvable)) {
-            await msg.channel.send("**ERROR:** insufficiant permissions to use this matrix command");
-            return;
-        }
-
-        let replyMessage = "";
-        try {
-            replyMessage = await Util.ParseCommand(actions[command], parameters, args);
-        } catch (e) {
-            replyMessage = "**ERROR:** " + e.message;
-        }
-
-        await msg.channel.send(replyMessage);
-    }
-
-    private DiscordModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: string, action: string) {
-        return async ({name}) => {
-            let allChannelMxids: string[] = [];
-            await Promise.all(discordChannel.guild.channels.map(async (chan) => {
-                try {
-                    const chanMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(chan);
-                    allChannelMxids = allChannelMxids.concat(chanMxids);
-                } catch (e) {
-                    // pass, non-text-channel
-                }
-            }));
-            let errorMsg = "";
-            await Promise.all(allChannelMxids.map(async (chanMxid) => {
-                const intent = this.bridge.getIntent();
-                try {
-                    await intent[funcKey](chanMxid, name);
-                } catch (e) {
-                    // maybe we don't have permission to kick/ban/unban...?
-                    errorMsg += `\nCouldn't ${funcKey} ${name} from ${chanMxid}`;
-                }
-            }));
-            if (errorMsg) {
-                throw Error(errorMsg);
-            }
-            return `${action} ${name}`;
-        };
-    }
-
     private async joinRoom(intent: Intent, roomIdOrAlias: string, member?: Discord.GuildMember): Promise<void> {
         let currentSchedule = JOIN_ROOM_SCHEDULE[0];
         const doJoin = async () => {
diff --git a/src/matrixtypes.ts b/src/matrixtypes.ts
index 3d54c2d5641aa0ab9f22262ed30a3ef3e4014db1..7535b59a89063a02e1f1946321550287f483dd94 100644
--- a/src/matrixtypes.ts
+++ b/src/matrixtypes.ts
@@ -42,6 +42,7 @@ export interface IMatrixEvent {
     unsigned?: any; // tslint:disable-line no-any
     origin_server_ts?: number;
     users?: any; // tslint:disable-line no-any
+    users_default?: any; // tslint:disable-line no-any
     notifications?: any; // tslint:disable-line no-any
 }
 
diff --git a/src/provisioner.ts b/src/provisioner.ts
index 9a871a1750fae09eb2f3592b24d1ba1488ccc55d..c5b53068274d89b3e6644dce70104cfb4bd4e98b 100644
--- a/src/provisioner.ts
+++ b/src/provisioner.ts
@@ -78,7 +78,7 @@ export class Provisioner {
         setTimeout(() => approveFn(false, true), timeout);
 
         await channel.send(`${requestor} on matrix would like to bridge this channel. Someone with permission` +
-            " to manage webhooks please reply with !approve or !deny in the next 5 minutes");
+            " to manage webhooks please reply with `!matrix approve` or `!matrix deny` in the next 5 minutes");
         return await deferP;
 
     }
diff --git a/src/util.ts b/src/util.ts
index aca4d651a2eb9780ff8d1a5666b389799edb10db..6edce065c7dcc17bc6830f89226b681726367d8e 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 matrix-appservice-discord
+Copyright 2018, 2019 matrix-appservice-discord
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -21,16 +21,21 @@ import { Buffer } from "buffer";
 import * as mime from "mime";
 import { Permissions } from "discord.js";
 import { DiscordBridgeConfig } from "./config";
+import { Client as MatrixClient } from "matrix-js-sdk";
+import { IMatrixEvent } from "./matrixtypes";
 
 const HTTP_OK = 200;
 
 import { Log } from "./log";
 const log = new Log("Util");
 
+type PERMISSIONTYPES = any; // tslint:disable-line no-any
+
 export interface ICommandAction {
-    params: string[];
     description?: string;
-    permission?: string;
+    help?: string;
+    params: string[];
+    permission?: PERMISSIONTYPES;
     run(params: any): Promise<any>; // tslint:disable-line no-any
 }
 
@@ -40,13 +45,15 @@ export interface ICommandActions {
 
 export interface ICommandParameter {
     description?: string;
-    get(param: string): Promise<any>; // tslint:disable-line no-any
+    get?(param: string): Promise<any>; // tslint:disable-line no-any
 }
 
 export interface ICommandParameters {
     [index: string]: ICommandParameter;
 }
 
+export type CommandPermissonCheck = (permission: PERMISSIONTYPES) => Promise<boolean | string>;
+
 export interface IPatternMap {
     [index: string]: string;
 }
@@ -228,19 +235,111 @@ export class Util {
         return Object.keys(matrixUsers)[0];
     }
 
-    public static async ParseCommand(action: ICommandAction, parameters: ICommandParameters, args: string[]) {
+    public static async HandleHelpCommand(
+        prefix: string,
+        actions: ICommandActions,
+        parameters: ICommandParameters,
+        args: string[],
+        permissionCheck?: CommandPermissonCheck,
+    ): Promise<string> {
+        let reply = "";
+        if (args[0]) {
+            const actionKey = args[0];
+            const action = actions[actionKey];
+            if (!actions[actionKey]) {
+                return `**ERROR:** unknown command! Try \`${prefix} help\` to see all commands`;
+            }
+            if (action.permission !== undefined && permissionCheck) {
+                const permCheck = await permissionCheck(action.permission);
+                if (typeof permCheck === "string") {
+                    return `**ERROR:** ${permCheck}`;
+                }
+                if (!permCheck) {
+                    return `**ERROR:** permission denied! Try \`${prefix} help\` to see all available commands`;
+                }
+            }
+            reply += `\`${prefix} ${actionKey}`;
+            for (const param of action.params) {
+                reply += ` <${param}>`;
+            }
+            reply += `\`: ${action.description}\n`;
+            if (action.help) {
+                reply += action.help;
+            }
+            return reply;
+        }
+        reply += "Available Commands:\n";
+        for (const actionKey of Object.keys(actions)) {
+            const action = actions[actionKey];
+            if (action.permission !== undefined && permissionCheck) {
+                const permCheck = await permissionCheck(action.permission);
+                if (typeof permCheck === "string" || !permCheck) {
+                    continue;
+                }
+            }
+            reply += ` - \`${prefix} ${actionKey}`;
+            for (const param of action.params) {
+                reply += ` <${param}>`;
+            }
+            reply += `\`: ${action.description}\n`;
+        }
+        reply += "\nParameters:\n";
+        for (const parameterKey of Object.keys(parameters)) {
+            const parameter = parameters[parameterKey];
+            reply += ` - \`<${parameterKey}>\`: ${parameter.description}\n`;
+        }
+        return reply;
+    }
+
+    public static async ParseCommand(
+        prefix: string,
+        msg: string,
+        actions: ICommandActions,
+        parameters: ICommandParameters,
+        permissionCheck?: CommandPermissonCheck,
+    ): Promise<string> {
+        const {command, args} = Util.MsgToArgs(msg, prefix);
+
+        if (command === "help") {
+            return await Util.HandleHelpCommand(prefix, actions, parameters, args, permissionCheck);
+        }
+
+        if (!actions[command]) {
+            return `**ERROR:** unknown command. Try \`${prefix} help\` to see all commands`;
+        }
+        const action = actions[command];
+        if (action.permission !== undefined && permissionCheck) {
+            const permCheck = await permissionCheck(action.permission);
+            if (typeof permCheck === "string") {
+                return `**ERROR:** ${permCheck}`;
+            }
+            if (!permCheck) {
+                return `**ERROR:** insufficiant permissions to use this command! ` +
+                    `Try \`${prefix} help\` to see all available commands`;
+            }
+        }
         if (action.params.length === 1) {
             args[0] = args.join(" ");
         }
-        const params = {};
-        let i = 0;
-        for (const param of action.params) {
-            params[param] = await parameters[param].get(args[i]);
-            i++;
-        }
+        try {
+            const params = {};
+            let i = 0;
+            for (const param of action.params) {
+                if (parameters[param].get !== undefined) {
+                    params[param] = await parameters[param].get!(args[i]);
+                } else {
+                    params[param] = args[i];
+                }
+                i++;
+            }
 
-        const retStr = await action.run(params);
-        return retStr;
+            const retStr = await action.run(params);
+            return retStr;
+        } catch (e) {
+            return `**ERROR:** ${e.message}`;
+            log.error("Error processing command");
+            log.error(e);
+        }
     }
 
     public static MsgToArgs(msg: string, prefix: string) {
@@ -285,6 +384,38 @@ export class Util {
         }
         return str;
     }
+
+    public static async CheckMatrixPermission(
+        mxClient: MatrixClient,
+        userId: string,
+        roomId: string,
+        defaultLevel: number,
+        cat: string,
+        subcat?: string,
+    ) {
+        const res: IMatrixEvent = await mxClient.getStateEvent(roomId, "m.room.power_levels");
+        let requiredLevel = defaultLevel;
+        if (res && (res[cat] || !subcat)) {
+            if (subcat) {
+                if (res[cat][subcat] !== undefined) {
+                    requiredLevel = res[cat][subcat];
+                }
+            } else {
+                if (res[cat] !== undefined) {
+                    requiredLevel = res[cat];
+                }
+            }
+        }
+
+        let haveLevel = 0;
+        if (res && res.users_default) {
+            haveLevel = res.users_default;
+        }
+        if (res && res.users && res.users[userId] !== undefined) {
+            haveLevel = res.users[userId];
+        }
+        return haveLevel >= requiredLevel;
+    }
 }
 
 interface IUploadResult {
diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts
index c50c76f502d4310e552499025979f66c3287777a..2eeccfc065c3506ed596029794b071f94654f6af 100644
--- a/test/test_discordbot.ts
+++ b/test/test_discordbot.ts
@@ -1,5 +1,5 @@
 /*
-Copyright 2017, 2018 matrix-appservice-discord
+Copyright 2017 - 2019 matrix-appservice-discord
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -133,13 +133,11 @@ describe("DiscordBot", () => {
     });
     describe("OnMessage()", () => {
         let SENT_MESSAGE = false;
-        let MARKED = -1;
         let HANDLE_COMMAND = false;
         let ATTACHMENT = {} as any;
         let MSGTYPE = "";
         function getDiscordBot() {
             SENT_MESSAGE = false;
-            MARKED = -1;
             HANDLE_COMMAND = false;
             ATTACHMENT = {};
             MSGTYPE = "";
@@ -150,13 +148,6 @@ describe("DiscordBot", () => {
                 {},
             );
             discord.bot = { user: { id: "654" } };
-            discord.provisioner = {
-                HasPendingRequest: (chan) => true,
-                MarkApproved: async (chan, member, approved) => {
-                    MARKED = approved ? 1 : 0;
-                    return approved;
-                },
-            };
             discord.GetIntentFromDiscordMember = (_) => {return {
                 sendMessage: async (room, msg) => {
                     SENT_MESSAGE = true;
@@ -175,8 +166,8 @@ describe("DiscordBot", () => {
             discord.channelSync = {
                 GetRoomIdsFromChannel: async (chan) => ["!asdf:localhost"],
             };
-            discord.roomHandler = {
-                HandleDiscordCommand: async (msg) => { HANDLE_COMMAND = true; },
+            discord.discordCommandHandler = {
+                Process: async (msg) => { HANDLE_COMMAND = true; },
             };
             discord.store = {
                 Insert: async (_) => { },
@@ -195,22 +186,6 @@ describe("DiscordBot", () => {
             await discordBot.OnMessage(msg);
             Chai.assert.equal(SENT_MESSAGE, false);
         });
-        it("accepts !approve", async () => {
-            discordBot = getDiscordBot();
-            const channel = new Discord.TextChannel({} as any, {} as any);
-            const msg = new MockMessage(channel) as any;
-            msg.content = "!approve";
-            await discordBot.OnMessage(msg);
-            Chai.assert.equal(MARKED, 1);
-        });
-        it("denies !deny", async () => {
-            discordBot = getDiscordBot();
-            const channel = new Discord.TextChannel({} as any, {} as any);
-            const msg = new MockMessage(channel) as any;
-            msg.content = "!deny";
-            await discordBot.OnMessage(msg);
-            Chai.assert.equal(MARKED, 0);
-        });
         it("Passes on !matrix commands", async () => {
             discordBot = getDiscordBot();
             const channel = new Discord.TextChannel({} as any, {} as any);
diff --git a/test/test_discordcommandhandler.ts b/test/test_discordcommandhandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f5851f4f4e1a5fb5bbdaa1f2632f61475ee3c89f
--- /dev/null
+++ b/test/test_discordcommandhandler.ts
@@ -0,0 +1,205 @@
+/*
+Copyright 2019 matrix-appservice-discord
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+import * as Chai from "chai";
+import * as Proxyquire from "proxyquire";
+
+import { DiscordCommandHandler } from "../src/discordcommandhandler";
+import { MockChannel } from "./mocks/channel";
+import { MockMember } from "./mocks/member";
+import { MockGuild } from "./mocks/guild";
+import { Util } from "../src/util";
+
+// we are a test file and thus need those
+/* tslint:disable:no-unused-expression max-file-line-count no-any */
+
+const expect = Chai.expect;
+
+let USERSJOINED = 0;
+let USERSKICKED = 0;
+let USERSBANNED = 0;
+let USERSUNBANNED = 0;
+let MESSAGESENT: any = {};
+let MARKED = -1;
+function createCH(opts: any = {}) {
+    USERSJOINED = 0;
+    USERSKICKED = 0;
+    USERSBANNED = 0;
+    USERSUNBANNED = 0;
+    MESSAGESENT = {};
+    MARKED = -1;
+    const bridge = {
+        getIntent: () => {
+            return {
+                ban: async () => { USERSBANNED++; },
+                getEvent: () => ({ content: { } }),
+                join: () => { USERSJOINED++; },
+                kick: async () => { USERSKICKED++; },
+                leave: () => { },
+                sendMessage: async (roomId, content) => { MESSAGESENT = content; return content; },
+                unban: async () => { USERSUNBANNED++; },
+            };
+        },
+    };
+    const cs = {
+        GetRoomIdsFromChannel: async (chan) => {
+            return [`#${chan.id}:localhost`];
+        },
+    };
+    const discord = {
+        ChannelSyncroniser: cs,
+        Provisioner: {
+            HasPendingRequest: (chan) => true,
+            MarkApproved: async (chan, member, approved) => {
+                MARKED = approved ? 1 : 0;
+                return approved;
+            },
+        },
+    };
+    const discordCommandHndlr = (Proxyquire("../src/discordcommandhandler", {
+        "./util": {
+            Util: {
+                GetMxidFromName: () => {
+                    return "@123456:localhost";
+                },
+                ParseCommand: Util.ParseCommand,
+            },
+        },
+    })).DiscordCommandHandler;
+    return new discordCommandHndlr(bridge as any, discord as any);
+}
+
+describe("DiscordCommandHandler", () => {
+    it("will kick a member", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return true;
+        };
+        const message = {
+            channel,
+            content: "!matrix kick someuser",
+            member,
+        };
+        await handler.Process(message);
+        expect(USERSKICKED).equals(1);
+    });
+    it("will kick a member in all guild rooms", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel, (new MockChannel("456"))]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return true;
+        };
+        const message = {
+            channel,
+            content: "!matrix kick someuser",
+            member,
+        };
+        await handler.Process(message);
+        // tslint:disable-next-line:no-magic-numbers
+        expect(USERSKICKED).equals(2);
+    });
+    it("will deny permission", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return false;
+        };
+        const message = {
+            channel,
+            content: "!matrix kick someuser",
+            member,
+        };
+        await handler.Process(message);
+        expect(USERSKICKED).equals(0);
+    });
+    it("will ban a member", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return true;
+        };
+        const message = {
+            channel,
+            content: "!matrix ban someuser",
+            member,
+        };
+        await handler.Process(message);
+        expect(USERSBANNED).equals(1);
+    });
+    it("will unban a member", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return true;
+        };
+        const message = {
+            channel,
+            content: "!matrix unban someuser",
+            member,
+        };
+        await handler.Process(message);
+        expect(USERSUNBANNED).equals(1);
+    });
+    it("handles !matrix approve", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return true;
+        };
+        const message = {
+            channel,
+            content: "!matrix approve",
+            member,
+        };
+        await handler.Process(message);
+        expect(MARKED).equals(1);
+    });
+    it("handles !matrix deny", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return true;
+        };
+        const message = {
+            channel,
+            content: "!matrix deny",
+            member,
+        };
+        await handler.Process(message);
+        expect(MARKED).equals(0);
+    });
+});
diff --git a/test/test_matrixcommandhandler.ts b/test/test_matrixcommandhandler.ts
index 8bef1b1462b331713d18f43a834cc69eb97587b0..70359d79ea92db5e15064a97fed075edc9589f11 100644
--- a/test/test_matrixcommandhandler.ts
+++ b/test/test_matrixcommandhandler.ts
@@ -1,7 +1,24 @@
+/*
+Copyright 2019 matrix-appservice-discord
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
 import * as Chai from "chai";
-import { MatrixCommandHandler } from "../src/matrixcommandhandler";
+import { Util } from "../src/util";
 import { DiscordBridgeConfig } from "../src/config";
 import { MockChannel } from "./mocks/channel";
+import * as Proxyquire from "proxyquire";
 
 // we are a test file and thus need those
 /* tslint:disable:no-unused-expression max-file-line-count no-any */
@@ -52,9 +69,6 @@ function createCH(opts: any = {}) {
         config.bridge.enableSelfServiceBridging = true;
     }
     const mxClient = {
-        getStateEvent: async () => {
-            return opts.powerLevels || {};
-        },
         getUserId: () => "@user:localhost",
         joinRoom: async () => {
             USERSJOINED++;
@@ -92,210 +106,121 @@ function createCH(opts: any = {}) {
         },
         Provisioner: provisioner,
     };
-    return new MatrixCommandHandler(bot as any, bridge, config);
+
+    const MatrixCommandHndl = (Proxyquire("../src/matrixcommandhandler", {
+        "./util": {
+            Util: {
+                CheckMatrixPermission: async () => {
+                    return opts.power !== undefined ? opts.power : true;
+                },
+                GetBotLink: Util.GetBotLink,
+                ParseCommand: Util.ParseCommand,
+            },
+        },
+    })).MatrixCommandHandler;
+    return new MatrixCommandHndl(bot as any, bridge, config);
+}
+
+function createEvent(msg: string, room?: string, userId?: string) {
+    return {
+        content: {
+            body: msg,
+        },
+        room_id: room ? room : "!123:localhost",
+        sender: userId,
+    };
+}
+
+function createContext(remoteData?: any) {
+    return {
+        rooms: {
+            remote: remoteData,
+        },
+    };
 }
 
 describe("MatrixCommandHandler", () => {
-    describe("ProcessCommand", () => {
+    describe("Process", () => {
         it("should not process command if not in room", async () => {
             const handler: any = createCH({disableSS: true});
-            const ret = await handler.ProcessCommand({
-                room_id: "!666:localhost",
-            });
-            expect(ret).to.be.undefined;
+            await handler.Process(createEvent("", "!666:localhost"), createContext());
+            expect(MESSAGESENT.body).to.equal(undefined);
         });
         it("should warn if self service is disabled", async () => {
             const handler: any = createCH({disableSS: true});
-            await handler.ProcessCommand({
-                room_id: "!123:localhost",
-            });
-            expect(MESSAGESENT.body).equals("The owner of this bridge does not permit self-service bridging.");
-        });
-        it("should warn if user is not powerful enough with defaults", async () => {
-            const handler: any = createCH();
-            await handler.ProcessCommand({
-                room_id: "!123:localhost",
-            });
-            expect(MESSAGESENT.body).equals("You do not have the required power level in this room to " +
-                "create a bridge to a Discord channel.");
-        });
-        it("should warn if user is not powerful enough with custom state default", async () => {
-            const handler: any = createCH({powerLevels: {
-                state_default: 67,
-            }});
-            await handler.ProcessCommand({
-                room_id: "!123:localhost",
-            });
-            expect(MESSAGESENT.body).equals("You do not have the required power level in this room to " +
-                "create a bridge to a Discord channel.");
-        });
-        it("should allow if user is powerful enough with defaults", async () => {
-            const handler: any = createCH({powerLevels: {
-                users_default: 60,
-            }});
-            const evt = await handler.ProcessCommand({
-                content: {body: "!discord help"},
-                room_id: "!123:localhost",
-            });
-            expect(evt.body.startsWith("Available commands")).to.be.true;
+            await handler.Process(createEvent("!discord bridge"), createContext());
+            expect(MESSAGESENT.body).to.equal("**ERROR:** The owner of this bridge does " +
+                "not permit self-service bridging.");
         });
-        it("should allow if user is powerful enough with their own state", async () => {
-            const handler: any = createCH({powerLevels: {
-                users: {
-                 "@user:localhost": 100,
-                },
-            }});
-            const evt = await handler.ProcessCommand({
-                content: {body: "!discord help"},
-                room_id: "!123:localhost",
-                sender: "@user:localhost",
+        it("should warn if user is not powerful enough", async () => {
+            const handler: any = createCH({
+                power: false,
             });
-            expect(evt.body.startsWith("Available commands")).to.be.true;
+            await handler.Process(createEvent("!discord bridge"), createContext());
+            expect(MESSAGESENT.body).to.equal("**ERROR:** insufficiant permissions to use this " +
+                "command! Try `!discord help` to see all available commands");
         });
         describe("!discord bridge", () => {
             it("will bridge a new room, and ask for permissions", async () => {
-                const handler: any = createCH({powerLevels: {
-                        users_default: 100,
-                    }});
-                const context = {rooms: {}};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord bridge 123 456"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("I have bridged this room to your channel");
+                const handler: any = createCH();
+                await handler.Process(createEvent("!discord bridge 123 456"), createContext());
+                expect(MESSAGESENT.body).to.equal("I have bridged this room to your channel");
             });
             it("will fail to bridge if permissions were denied", async () => {
                 const handler: any = createCH({
                     denyBridgePermission: true,
-                    powerLevels: {
-                        users_default: 100,
-                    },
                 });
-                const context = {rooms: {}};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord bridge 123 456"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("The bridge has been declined by the Discord guild");
+                await handler.Process(createEvent("!discord bridge 123 456"), createContext());
+                expect(MESSAGESENT.body).to.equal("The bridge has been declined by the Discord guild");
             });
-            it("will fail to bridge if permissions were denied", async () => {
+            it("will fail to bridge if permissions were failed", async () => {
                 const handler: any = createCH({
                     failBridgeMatrix: true,
-                    powerLevels: {
-                        users_default: 100,
-                    },
                 });
-                const context = {rooms: {}};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord bridge 123 456"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("There was a problem bridging that channel - has " +
+                const evt = await handler.Process(createEvent("!discord bridge 123 456"), createContext());
+                expect(MESSAGESENT.body).to.equal("There was a problem bridging that channel - has " +
                     "the guild owner approved the bridge?");
             });
             it("will not bridge if a link already exists", async () => {
-                const handler: any = createCH({
-                    powerLevels: {
-                        users_default: 100,
-                    },
-                });
-                const context = {rooms: { remote: true }};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord bridge"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("This room is already bridged to a Discord guild.");
+                const handler: any = createCH();
+                const evt = await handler.Process(createEvent("!discord bridge 123 456"), createContext(true));
+                expect(MESSAGESENT.body).to.equal("This room is already bridged to a Discord guild.");
             });
             it("will not bridge without required args", async () => {
-                const handler: any = createCH({
-                    powerLevels: {
-                        users_default: 100,
-                    },
-                });
-                const context = {rooms: {}};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord bridge"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).to.contain("Invalid syntax");
+                const handler: any = createCH();
+                const evt = await handler.Process(createEvent("!discord bridge"), createContext());
+                expect(MESSAGESENT.body).to.contain("Invalid syntax");
             });
             it("will bridge with x/y syntax", async () => {
                 const handler: any = createCH({powerLevels: {
                         users_default: 100,
                     }});
-                const context = {rooms: {}};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord bridge 123/456"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("I have bridged this room to your channel");
+                const evt = await handler.Process(createEvent("!discord bridge 123/456"), createContext());
+                expect(MESSAGESENT.body).equals("I have bridged this room to your channel");
             });
         });
         describe("!discord unbridge", () => {
             it("will unbridge", async () => {
-                const handler: any = createCH({
-                    powerLevels: {
-                        users_default: 100,
-                    },
-                });
-                const context = {rooms: { remote: {
-                    data: {
-                        plumbed: true,
-                    },
-                } }};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord unbridge"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("This room has been unbridged");
+                const handler: any = createCH();
+                await handler.Process(createEvent("!discord unbridge"), createContext({data: {plumbed: true}}));
+                expect(MESSAGESENT.body).equals("This room has been unbridged");
             });
             it("will not unbridge if a link does not exist", async () => {
-                const handler: any = createCH({
-                    powerLevels: {
-                        users_default: 100,
-                    },
-                });
-                const context = {rooms: { remote: undefined }};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord unbridge"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("This room is not bridged.");
+                const handler: any = createCH();
+                const evt = await handler.Process(createEvent("!discord unbridge"), createContext());
+                expect(MESSAGESENT.body).equals("This room is not bridged.");
             });
             it("will not unbridge non-plumbed rooms", async () => {
-                const handler: any = createCH({
-                    powerLevels: {
-                        users_default: 100,
-                    },
-                });
-                const context = {rooms: { remote: {
-                    data: {
-                        plumbed: false,
-                    },
-                }}};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord unbridge"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).equals("This room cannot be unbridged.");
+                const handler: any = createCH();
+                await handler.Process(createEvent("!discord unbridge"), createContext({data: {plumbed: false}}));
+                expect(MESSAGESENT.body).equals("This room cannot be unbridged.");
             });
             it("will show error if unbridge fails", async () => {
                 const handler: any = createCH({
                     failUnbridge: true,
-                    powerLevels: {
-                        users_default: 100,
-                    },
                 });
-                const context = {rooms: { remote: {
-                    data: {
-                        plumbed: true,
-                    },
-                }}};
-                const evt = await handler.ProcessCommand({
-                    content: {body: "!discord unbridge"},
-                    room_id: "!123:localhost",
-                }, context);
-                expect(evt.body).to.contain("There was an error unbridging this room.");
+                await handler.Process(createEvent("!discord unbridge"), createContext({data: {plumbed: true}}));
+                expect(MESSAGESENT.body).to.contain("There was an error unbridging this room.");
             });
         });
     });
diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts
index 71ec84bf415758f4abb0e4b1e8a7f234c261802f..2c836b295847504637a4fac4361557bc47be5595 100644
--- a/test/test_matrixeventprocessor.ts
+++ b/test/test_matrixeventprocessor.ts
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 matrix-appservice-discord
+Copyright 2018, 2019 matrix-appservice-discord
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -94,7 +94,7 @@ function createMatrixEventProcessor(): MatrixEventProcessor {
     USERSYNC_HANDLED = false;
     STATE_EVENT_MSG = "";
     MESSAGE_PROCCESS = "";
-    KICKBAN_HANDLED = true;
+    KICKBAN_HANDLED = false;
     const bridge = {
         getBot: () => {
             return {
@@ -230,7 +230,7 @@ function createMatrixEventProcessor(): MatrixEventProcessor {
         HandleInvite: async (evt) => {
             MESSAGE_PROCCESS = "invited";
         },
-        ProcessCommand: async (evt) => {
+        Process: async (evt) => {
             MESSAGE_PROCCESS = "command_processed";
         },
     });
diff --git a/test/test_matrixroomhandler.ts b/test/test_matrixroomhandler.ts
index 4c5de12bb552fc923823a55044857f3531b394ad..d8fa8d04ab87923b35586e366cb775d55d3fe894 100644
--- a/test/test_matrixroomhandler.ts
+++ b/test/test_matrixroomhandler.ts
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 matrix-appservice-discord
+Copyright 2018, 2019 matrix-appservice-discord
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,10 +16,10 @@ limitations under the License.
 
 import * as Chai from "chai";
 import * as Proxyquire from "proxyquire";
-import {DiscordBridgeConfig} from "../src/config";
-import {MockChannel} from "./mocks/channel";
-import {MockMember} from "./mocks/member";
-import {MockGuild} from "./mocks/guild";
+import { DiscordBridgeConfig } from "../src/config";
+import { MockChannel } from "./mocks/channel";
+import { MockMember } from "./mocks/member";
+import { MockGuild } from "./mocks/guild";
 import { Util } from "../src/util";
 
 // we are a test file and thus need those
@@ -54,6 +54,7 @@ function createRH(opts: any = {}) {
     USERSKICKED = 0;
     USERSBANNED = 0;
     USERSUNBANNED = 0;
+    MESSAGESENT = {};
     USERSYNC_HANDLED = false;
     KICKBAN_HANDLED = false;
     MESSAGE_PROCCESS = "";
@@ -354,92 +355,4 @@ describe("MatrixRoomHandler", () => {
             expect(roomOpts.creationOpts).to.exist;
         });
     });
-    describe("HandleDiscordCommand", () => {
-        it("will kick a member", async () => {
-            const handler: any = createRH({});
-            const channel = new MockChannel("123");
-            const guild = new MockGuild("456", [channel]);
-            channel.guild = guild;
-            const member: any = new MockMember("123456", "blah");
-            member.hasPermission = () => {
-                return true;
-            };
-            const message = {
-                channel,
-                content: "!matrix kick someuser",
-                member,
-            };
-            await handler.HandleDiscordCommand(message);
-            expect(USERSKICKED).equals(1);
-        });
-        it("will kick a member in all guild rooms", async () => {
-            const handler: any = createRH({});
-            const channel = new MockChannel("123");
-            const guild = new MockGuild("456", [channel, (new MockChannel("456"))]);
-            channel.guild = guild;
-            const member: any = new MockMember("123456", "blah");
-            member.hasPermission = () => {
-                return true;
-            };
-            const message = {
-                channel,
-                content: "!matrix kick someuser",
-                member,
-            };
-            await handler.HandleDiscordCommand(message);
-            // tslint:disable-next-line:no-magic-numbers
-            expect(USERSKICKED).equals(2);
-        });
-        it("will deny permission", async () => {
-            const handler: any = createRH({});
-            const channel = new MockChannel("123");
-            const guild = new MockGuild("456", [channel]);
-            channel.guild = guild;
-            const member: any = new MockMember("123456", "blah");
-            member.hasPermission = () => {
-                return false;
-            };
-            const message = {
-                channel,
-                content: "!matrix kick someuser",
-                member,
-            };
-            await handler.HandleDiscordCommand(message);
-            expect(USERSKICKED).equals(0);
-        });
-        it("will ban a member", async () => {
-            const handler: any = createRH({});
-            const channel = new MockChannel("123");
-            const guild = new MockGuild("456", [channel]);
-            channel.guild = guild;
-            const member: any = new MockMember("123456", "blah");
-            member.hasPermission = () => {
-                return true;
-            };
-            const message = {
-                channel,
-                content: "!matrix ban someuser",
-                member,
-            };
-            await handler.HandleDiscordCommand(message);
-            expect(USERSBANNED).equals(1);
-        });
-        it("will unban a member", async () => {
-            const handler: any = createRH({});
-            const channel = new MockChannel("123");
-            const guild = new MockGuild("456", [channel]);
-            channel.guild = guild;
-            const member: any = new MockMember("123456", "blah");
-            member.hasPermission = () => {
-                return true;
-            };
-            const message = {
-                channel,
-                content: "!matrix unban someuser",
-                member,
-            };
-            await handler.HandleDiscordCommand(message);
-            expect(USERSUNBANNED).equals(1);
-        });
-    });
 });
diff --git a/test/test_util.ts b/test/test_util.ts
index 17664269950452f36138dacec1f8ad3e0ce13a97..2e4ca1a3f0e8421f78e5400a6a2a587a1b6ec00d 100644
--- a/test/test_util.ts
+++ b/test/test_util.ts
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 matrix-appservice-discord
+Copyright 2018, 2019 matrix-appservice-discord
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@ limitations under the License.
 
 import * as Chai from "chai";
 
-import { Util, ICommandAction, ICommandParameters } from "../src/util";
+import { Util, ICommandActions, ICommandParameters } from "../src/util";
 
 // we are a test file and thus need those
 /* tslint:disable:no-unused-expression max-file-line-count no-any */
@@ -60,28 +60,72 @@ describe("Util", () => {
             Chai.assert.equal(args[1], "arg2");
         });
     });
-    describe("ParseCommand", () => {
-        it("parses commands", async () => {
-            const action: ICommandAction = {
+    describe("Command Stuff", () => {
+        const actions: ICommandActions = {
+            action: {
+                description: "floof",
+                help: "Fox goes floof!",
                 params: ["param1", "param2"],
                 run: async ({param1, param2}) => {
                     return `param1: ${param1}\nparam2: ${param2}`;
                 },
-            };
-            const parameters: ICommandParameters = {
-                param1: {
-                    get: async (param: string) => {
-                        return "param1_" + param;
-                    },
+            },
+        };
+        const parameters: ICommandParameters = {
+            param1: {
+                description: "1",
+                get: async (param: string) => {
+                    return "param1_" + param;
                 },
-                param2: {
-                    get: async (param: string) => {
-                        return "param2_" + param;
-                    },
+            },
+            param2: {
+                description: "2",
+                get: async (param: string) => {
+                    return "param2_" + param;
                 },
-            };
-            const retStr = await Util.ParseCommand(action, parameters, ["hello", "world"]);
-            expect(retStr).equal("param1: param1_hello\nparam2: param2_world");
+            },
+        };
+        describe("HandleHelpCommand", () => {
+            it("parses general help message", async () => {
+                const {command, args} = Util.MsgToArgs("!fox help", "!fox");
+                const retStr = await Util.HandleHelpCommand(
+                    "!fox",
+                    actions,
+                    parameters,
+                    args,
+                );
+                expect(retStr).to.equal(
+`Available Commands:
+ - \`!fox action <param1> <param2>\`: floof
+
+Parameters:
+ - \`<param1>\`: 1
+ - \`<param2>\`: 2
+`);
+            });
+            it("parses specific help message", async () => {
+                const {command, args} = Util.MsgToArgs("!fox help action", "!fox");
+                const retStr = await Util.HandleHelpCommand(
+                    "!fox",
+                    actions,
+                    parameters,
+                    args,
+                );
+                expect(retStr).to.equal(
+`\`!fox action <param1> <param2>\`: floof
+Fox goes floof!`);
+            });
+        });
+        describe("ParseCommand", () => {
+            it("parses commands", async () => {
+                const retStr = await Util.ParseCommand(
+                    "!fox",
+                    "!fox action hello world",
+                    actions,
+                    parameters,
+                );
+                expect(retStr).equal("param1: param1_hello\nparam2: param2_world");
+            });
         });
     });
     describe("GetMxidFromName", () => {
@@ -187,4 +231,84 @@ describe("Util", () => {
             expect(Date.now()).to.be.greaterThan(t + DELAY_FOR - 1);
         });
     });
+    describe("CheckMatrixPermission", () => {
+        const PERM_LEVEL = 50;
+        it("should deny", async () => {
+            const ret = await Util.CheckMatrixPermission(
+                {
+                    getStateEvent: async () => {
+                        return {
+                            blah: {
+                                blubb: PERM_LEVEL,
+                            },
+                        };
+                    },
+                } as any,
+                "@user:localhost",
+                "",
+                PERM_LEVEL,
+                "blah",
+                "blubb",
+            );
+            expect(ret).to.be.false;
+        });
+        it("should allow cat/subcat", async () => {
+            const ret = await Util.CheckMatrixPermission(
+                {
+                    getStateEvent: async () => {
+                        return {
+                            blah: {
+                                blubb: PERM_LEVEL,
+                            },
+                            users: {
+                                "@user:localhost": PERM_LEVEL,
+                            },
+                        };
+                    },
+                } as any,
+                "@user:localhost",
+                "",
+                PERM_LEVEL,
+                "blah",
+                "blubb",
+            );
+            expect(ret).to.be.true;
+        });
+        it("should allow cat", async () => {
+            const ret = await Util.CheckMatrixPermission(
+                {
+                    getStateEvent: async () => {
+                        return {
+                            blah: PERM_LEVEL,
+                            users: {
+                                "@user:localhost": PERM_LEVEL,
+                            },
+                        };
+                    },
+                } as any,
+                "@user:localhost",
+                "",
+                PERM_LEVEL,
+                "blah",
+            );
+            expect(ret).to.be.true;
+        });
+        it("should allow based on default", async () => {
+            const ret = await Util.CheckMatrixPermission(
+                {
+                    getStateEvent: async () => {
+                        return {
+                            blah: PERM_LEVEL,
+                            users_default: PERM_LEVEL,
+                        };
+                    },
+                } as any,
+                "@user:localhost",
+                "",
+                PERM_LEVEL,
+                "blah",
+            );
+            expect(ret).to.be.true;
+        });
+    });
 });