diff --git a/src/bot.ts b/src/bot.ts
index 48487fe31f0bdeba3948fefb37b297b782a6e0e3..a77c32ed0f1714c076dffe06fd19faa7cf69198a 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -26,7 +26,7 @@ import {
     DiscordMessageProcessorOpts,
     DiscordMessageProcessorResult,
 } from "./discordmessageprocessor";
-import { MatrixEventProcessor, MatrixEventProcessorOpts } from "./matrixeventprocessor";
+import { MatrixEventProcessor, MatrixEventProcessorOpts, IMatrixEventProcessorResult } from "./matrixeventprocessor";
 import { PresenceHandler } from "./presencehandler";
 import { Provisioner } from "./provisioner";
 import { UserSyncroniser } from "./usersyncroniser";
@@ -125,6 +125,14 @@ export class DiscordBot {
         return this.roomHandler;
     }
 
+    get MxEventProcessor(): MatrixEventProcessor {
+        return this.mxEventProcessor;
+    }
+
+    get Provisioner(): Provisioner {
+        return this.provisioner;
+    }
+
     public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.User, webhookID?: string): Intent {
         if (webhookID) {
             // webhookID and user IDs are the same, they are unique, so no need to prefix _webhook_
@@ -336,53 +344,23 @@ export class DiscordBot {
         }
     }
 
-    public async ProcessMatrixStateEvent(event: IMatrixEvent): Promise<void> {
-        log.verbose(`Got state event from ${event.room_id} ${event.type}`);
-        const channel = await this.GetChannelFromRoomId(event.room_id) as Discord.TextChannel;
-        const msg = this.mxEventProcessor.StateEventToMessage(event, channel);
+    public async sendAsBot(msg: string, channel: Discord.TextChannel, event: IMatrixEvent): Promise<void> {
         if (!msg) {
             return;
         }
-        let res = await channel.send(msg);
-        if (!Array.isArray(res)) {
-            res = [res];
-        }
-        await Util.AsyncForEach(res, async (m: Discord.Message) => {
-            log.verbose("Sent (state msg) ", m.id);
-            this.sentMessages.push(m.id);
-            this.lastEventIds[event.room_id] = event.event_id;
-            const evt = new DbEvent();
-            evt.MatrixId = `${event.event_id};${event.room_id}`;
-            evt.DiscordId = m.id;
-            evt.GuildId = channel.guild.id;
-            evt.ChannelId = channel.id;
-            await this.store.Insert(evt);
-        });
-        if (!this.config.bridge.disableReadReceipts) {
-            try {
-                await this.bridge.getIntent().sendReadReceipt(event.room_id, event.event_id);
-            } catch (err) {
-                log.error(`Failed to send read receipt for ${event}. `, err);
-            }
-        }
+        const res = await channel.send(msg);
+        await this.StoreMessagesSent(res, channel, event);
     }
 
-    public async ProcessMatrixMsgEvent(event: IMatrixEvent, guildId: string, channelId: string): Promise<void> {
-        const mxClient = this.bridge.getClientFactory().getClientAs();
-        log.verbose(`Looking up ${guildId}_${channelId}`);
-        const result = await this.LookupRoom(guildId, channelId, event.sender);
-        const chan = result.channel;
-        const botUser = result.botUser;
-
-        const embedSet = await this.mxEventProcessor.EventToEmbed(event, chan);
+    public async send(
+        embedSet: IMatrixEventProcessorResult,
+        opts: Discord.MessageOptions,
+        roomLookup: ChannelLookupResult,
+        event: IMatrixEvent,
+    ): Promise<void> {
+        const chan = roomLookup.channel;
+        const botUser = roomLookup.botUser;
         const embed = embedSet.messageEmbed;
-        const opts: Discord.MessageOptions = {};
-        const file = await this.mxEventProcessor.HandleAttachment(event, mxClient);
-        if (typeof(file) === "string") {
-            embed.description += " " + file;
-        } else {
-            opts.file = file;
-        }
 
         let msg: Discord.Message | null | (Discord.Message | null)[] = null;
         let hook: Discord.Webhook | undefined;
@@ -420,32 +398,10 @@ export class DiscordBot {
                 opts.embed = embed;
                 msg = await chan.send("", opts);
             }
+            await this.StoreMessagesSent(msg, chan, event);
         } catch (err) {
             log.error("Couldn't send message. ", err);
         }
-        if (!Array.isArray(msg)) {
-            msg = [msg];
-        }
-        await Util.AsyncForEach(msg, async (m: Discord.Message) => {
-            log.verbose("Sent ", m.id);
-            this.sentMessages.push(m.id);
-            this.lastEventIds[event.room_id] = event.event_id;
-            const evt = new DbEvent();
-            evt.MatrixId = `${event.event_id};${event.room_id}`;
-            evt.DiscordId = m.id;
-            // Webhooks don't send guild info.
-            evt.GuildId = guildId;
-            evt.ChannelId = channelId;
-            await this.store.Insert(evt);
-        });
-        if (!this.config.bridge.disableReadReceipts) {
-            try {
-                await this.bridge.getIntent().sendReadReceipt(event.room_id, event.event_id);
-            } catch (err) {
-                log.error(`Failed to send read receipt for ${event}. `, err);
-            }
-        }
-        return;
     }
 
     public async ProcessMatrixRedact(event: IMatrixEvent) {
@@ -910,4 +866,32 @@ export class DiscordBot {
             }
         }
     }
+
+    private async StoreMessagesSent(
+        msg: Discord.Message | null | (Discord.Message | null)[],
+        chan: Discord.TextChannel,
+        event: IMatrixEvent,
+    ) {
+        if (!Array.isArray(msg)) {
+            msg = [msg];
+        }
+        await Util.AsyncForEach(msg, async (m: Discord.Message) => {
+            if (!m) {
+                return;
+            }
+            log.verbose("Sent ", m.id);
+            this.sentMessages.push(m.id);
+            this.lastEventIds[event.room_id] = event.event_id;
+            try {
+                const evt = new DbEvent();
+                evt.MatrixId = `${event.event_id};${event.room_id}`;
+                evt.DiscordId = m.id;
+                evt.GuildId = chan.guild.id;
+                evt.ChannelId = chan.id;
+                await this.store.Insert(evt);
+            } catch (err) {
+                log.error(`Failed to insert sent event (${event.event_id};${event.room_id}) into store`, err);
+            }
+        });
+    }
 }
diff --git a/src/discordas.ts b/src/discordas.ts
index aa237f5ee1464ff18fb1c3dd1cbf6e985f2d152a..0f4b5e11d6a46c238e1e259316d23b11e06c4da2 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -168,11 +168,12 @@ async function run(port: number, fileConfig: DiscordBridgeConfig) {
 
     const discordbot = new DiscordBot(botUserId, config, bridge, store);
     const roomhandler = discordbot.RoomHandler;
+    const eventProcessor = discordbot.MxEventProcessor;
 
     try {
         callbacks.onAliasQueried = roomhandler.OnAliasQueried.bind(roomhandler);
         callbacks.onAliasQuery = roomhandler.OnAliasQuery.bind(roomhandler);
-        callbacks.onEvent = roomhandler.OnEvent.bind(roomhandler);
+        callbacks.onEvent = eventProcessor.OnEvent.bind(eventProcessor);
         callbacks.thirdPartyLookup = async () => {
             return roomhandler.ThirdPartyLookup;
         };
diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a116d6f5f127220f2cbb347e5a6906a024b4e1ce
--- /dev/null
+++ b/src/matrixcommandhandler.ts
@@ -0,0 +1,225 @@
+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 * as Discord from "discord.js";
+const log = new Log("MatrixCommandHandler");
+
+/* tslint:disable:no-magic-numbers */
+const PROVISIONING_DEFAULT_POWER_LEVEL = 50;
+const PROVISIONING_DEFAULT_USER_POWER_LEVEL = 0;
+const ROOM_CACHE_MAXAGE_MS = 15 * 60 * 1000;
+/* tslint:enable:no-magic-numbers */
+
+export class MatrixCommandHandler {
+    private botJoinedRooms: Set<string> = new Set(); // roomids
+    private botJoinedRoomsCacheUpdatedAt = 0;
+    private provisioner: Provisioner;
+    constructor(
+        private discord: DiscordBot,
+        private bridge: Bridge,
+        private config: DiscordBridgeConfig,
+    ) {
+        this.provisioner = this.discord.Provisioner;
+    }
+
+    public async HandleInvite(event: IMatrixEvent) {
+        log.info(`Received invite for ${event.state_key} in room ${event.room_id}`);
+        if (event.state_key === this.discord.GetBotId()) {
+            log.info("Accepting invite for bridge bot");
+            await this.bridge.getIntent().joinRoom(event.room_id);
+            this.botJoinedRooms.add(event.room_id);
+        }
+    }
+
+    public async ProcessCommand(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",
+                });
+            }
+
+            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
+        }
+    }
+
+    private async isBotInRoom(roomId: string): Promise<boolean> {
+        // Update the room cache, if not done already.
+        if (Date.now () - this.botJoinedRoomsCacheUpdatedAt > ROOM_CACHE_MAXAGE_MS) {
+            log.verbose("Updating room cache for bot...");
+            try {
+                log.verbose("Got new room cache for bot");
+                this.botJoinedRoomsCacheUpdatedAt = Date.now();
+                const rooms = (await this.bridge.getBot().getJoinedRooms()) as string[];
+                this.botJoinedRooms = new Set(rooms);
+            } catch (e) {
+                log.error("Failed to get room cache for bot, ", e);
+                return false;
+            }
+        }
+        return this.botJoinedRooms.has(roomId);
+    }
+}
diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts
index 2b23992eff3ce72f73f4ca0726cde6bbc7d3e921..28945add25b48c88d2916a52682cc2e429a99d63 100644
--- a/src/matrixeventprocessor.ts
+++ b/src/matrixeventprocessor.ts
@@ -21,10 +21,11 @@ import * as escapeStringRegexp from "escape-string-regexp";
 import { Util } from "./util";
 import * as path from "path";
 import * as mime from "mime";
-import { MatrixUser, Bridge } from "matrix-appservice-bridge";
+import { MatrixUser, Bridge, BridgeContext } from "matrix-appservice-bridge";
 import { Client as MatrixClient } from "matrix-js-sdk";
 import { IMatrixEvent, IMatrixEventContent, IMatrixMessage } from "./matrixtypes";
 import { MatrixMessageProcessor, IMatrixMessageProcessorParams } from "./matrixmessageprocessor";
+import { MatrixCommandHandler } from "./matrixcommandhandler";
 
 import { Log } from "./log";
 const log = new Log("MatrixEventProcessor");
@@ -34,6 +35,8 @@ const MIN_NAME_LENGTH = 2;
 const MAX_NAME_LENGTH = 32;
 const DISCORD_AVATAR_WIDTH = 128;
 const DISCORD_AVATAR_HEIGHT = 128;
+const ROOM_NAME_PARTS = 2;
+const AGE_LIMIT = 900000; // 15 * 60 * 1000
 
 export class MatrixEventProcessorOpts {
     constructor(
@@ -55,15 +58,131 @@ export class MatrixEventProcessor {
     private bridge: Bridge;
     private discord: DiscordBot;
     private matrixMsgProcessor: MatrixMessageProcessor;
+    private mxCommandHandler: MatrixCommandHandler;
 
-    constructor(opts: MatrixEventProcessorOpts) {
+    constructor(opts: MatrixEventProcessorOpts, cm?: MatrixCommandHandler) {
         this.config = opts.config;
         this.bridge = opts.bridge;
         this.discord = opts.discord;
         this.matrixMsgProcessor = new MatrixMessageProcessor(this.discord);
+        if (cm) {
+            this.mxCommandHandler = cm;
+        } else {
+            this.mxCommandHandler = new MatrixCommandHandler(this.discord, this.bridge, this.config);
+        }
+    }
+
+    public async OnEvent(request, context: BridgeContext): Promise<void> {
+        const event = request.getData() as IMatrixEvent;
+        if (event.unsigned.age > AGE_LIMIT) {
+            log.warn(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`);
+            return;
+        }
+        if (
+            event.type === "m.room.member" &&
+            event.content!.membership === "invite" &&
+            event.state_key === this.discord.GetBotId()
+        ) {
+            await this.mxCommandHandler.HandleInvite(event);
+            return;
+        } else if (event.type === "m.room.member" && this.bridge.getBot().isRemoteUser(event.state_key)) {
+            if (["leave", "ban"].includes(event.content!.membership!) && event.sender !== event.state_key) {
+                // Kick/Ban handling
+                let prevMembership = "";
+                if (event.content!.membership === "leave") {
+                    const intent = this.bridge.getIntent();
+                    prevMembership = (await intent.getEvent(event.room_id, event.replaces_state)).content.membership;
+                }
+                await this.discord.HandleMatrixKickBan(
+                    event.room_id,
+                    event.state_key,
+                    event.sender,
+                    event.content!.membership as "leave"|"ban",
+                    prevMembership,
+                    event.content!.reason,
+                );
+            }
+            return;
+        } else if (["m.room.member", "m.room.name", "m.room.topic"].includes(event.type)) {
+            await this.ProcessStateEvent(event);
+            return;
+        } else if (event.type === "m.room.redaction" && context.rooms.remote) {
+            await this.discord.ProcessMatrixRedact(event);
+            return;
+        } else if (event.type === "m.room.message" || event.type === "m.sticker") {
+            log.verbose(`Got ${event.type} event`);
+            const isBotCommand = event.type === "m.room.message" &&
+                event.content!.body &&
+                event.content!.body!.startsWith("!discord");
+            if (isBotCommand) {
+                await this.mxCommandHandler.ProcessCommand(event, context);
+                return;
+            } else if (context.rooms.remote) {
+                const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS);
+                try {
+                    await this.ProcessMsgEvent(event, srvChanPair[0], srvChanPair[1]);
+                    return;
+                } catch (err) {
+                    log.warn("There was an error sending a matrix event", err);
+                    return;
+                }
+            }
+        } else if (event.type === "m.room.encryption" && context.rooms.remote) {
+            try {
+                await this.HandleEncryptionWarning(event.room_id);
+                return;
+            } catch (err) {
+                throw new Error(`Failed to handle encrypted room, ${err}`);
+            }
+        } else {
+            log.verbose("Got non m.room.message event");
+        }
+        log.verbose("Event not processed by bridge");
+    }
+
+    public async HandleEncryptionWarning(roomId: string): Promise<void> {
+        const intent = this.bridge.getIntent();
+        log.info(`User has turned on encryption in ${roomId}, so leaving.`);
+        /* N.B 'status' is not specced but https://github.com/matrix-org/matrix-doc/pull/828
+         has been open for over a year with no resolution. */
+        const sendPromise = intent.sendMessage(roomId, {
+            body: "You have turned on encryption in this room, so the service will not bridge any new messages.",
+            msgtype: "m.notice",
+            status: "critical",
+        });
+        const channel = await this.discord.GetChannelFromRoomId(roomId);
+        await (channel as Discord.TextChannel).send(
+          "Someone on Matrix has turned on encryption in this room, so the service will not bridge any new messages",
+        );
+        await sendPromise;
+        await intent.leave(roomId);
+        await this.bridge.getRoomStore().removeEntriesByMatrixRoomId(roomId);
+    }
+
+    public async ProcessMsgEvent(event: IMatrixEvent, guildId: string, channelId: string) {
+        const mxClient = this.bridge.getClientFactory().getClientAs();
+        log.verbose(`Looking up ${guildId}_${channelId}`);
+        const roomLookup = await this.discord.LookupRoom(guildId, channelId, event.sender);
+        const chan = roomLookup.channel;
+        const botUser = roomLookup.botUser;
+
+        const embedSet = await this.EventToEmbed(event, chan);
+        const opts: Discord.MessageOptions = {};
+        const file = await this.HandleAttachment(event, mxClient);
+        if (typeof(file) === "string") {
+            embedSet.messageEmbed.description += " " + file;
+        } else {
+            opts.file = file;
+        }
+
+        await this.discord.send(embedSet, opts, roomLookup, event);
+        await this.sendReadReceipt(event);
     }
 
-    public StateEventToMessage(event: IMatrixEvent, channel: Discord.TextChannel): string | undefined {
+    public async ProcessStateEvent(event: IMatrixEvent) {
+        log.verbose(`Got state event from ${event.room_id} ${event.type}`);
+        const channel = await this.discord.GetChannelFromRoomId(event.room_id) as Discord.TextChannel;
+
         const SUPPORTED_EVENTS = ["m.room.member", "m.room.name", "m.room.topic"];
         if (!SUPPORTED_EVENTS.includes(event.type)) {
             log.verbose(`${event.event_id} ${event.type} is not displayable.`);
@@ -106,7 +225,8 @@ export class MatrixEventProcessor {
         }
 
         msg += " on Matrix.";
-        return msg;
+        await this.discord.sendAsBot(msg, channel, event);
+        await this.sendReadReceipt(event);
     }
 
     public async EventToEmbed(
@@ -250,6 +370,16 @@ export class MatrixEventProcessor {
         return embed;
     }
 
+    private async sendReadReceipt(event: IMatrixEvent) {
+        if (!this.config.bridge.disableReadReceipts) {
+            try {
+                await this.bridge.getIntent().sendReadReceipt(event.room_id, event.event_id);
+            } catch (err) {
+                log.error(`Failed to send read receipt for ${event}. `, err);
+            }
+        }
+    }
+
     private HasAttachment(event: IMatrixEvent): boolean {
         if (!event.content) {
             event.content = {};
diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts
index 564fdab9c08ed1d166cf3e644d76be952e3114b1..c648f6e503884cd2395bf076b76a375959c72fd9 100644
--- a/src/matrixroomhandler.ts
+++ b/src/matrixroomhandler.ts
@@ -22,7 +22,6 @@ import {
     thirdPartyProtocolResult,
     thirdPartyUserResult,
     thirdPartyLocationResult,
-    BridgeContext,
     ProvisionedRoom,
     Intent,
 } from "matrix-appservice-bridge";
@@ -40,7 +39,6 @@ const ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org/mlxoES
 /* tslint:disable:no-magic-numbers */
 const HTTP_UNSUPPORTED = 501;
 const ROOM_NAME_PARTS = 2;
-const AGE_LIMIT = 900000; // 15 * 60 * 1000
 const PROVISIONING_DEFAULT_POWER_LEVEL = 50;
 const PROVISIONING_DEFAULT_USER_POWER_LEVEL = 0;
 const USERSYNC_STATE_DELAY_MS = 5000;
@@ -138,271 +136,6 @@ export class MatrixRoomHandler {
         await Promise.all(promiseList);
     }
 
-    public async OnEvent(request, context: BridgeContext): Promise<void> {
-        const event = request.getData() as IMatrixEvent;
-        if (event.unsigned.age > AGE_LIMIT) {
-            log.warn(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`);
-            return;
-        }
-        if (event.type === "m.room.member" && event.content!.membership === "invite") {
-            await this.HandleInvite(event);
-            return;
-        } else if (event.type === "m.room.member" && this.bridge.getBot().isRemoteUser(event.state_key)) {
-            if (["leave", "ban"].includes(event.content!.membership!) && event.sender !== event.state_key) {
-                // Kick/Ban handling
-                let prevMembership = "";
-                if (event.content!.membership === "leave") {
-                    const intent = this.bridge.getIntent();
-                    prevMembership = (await intent.getEvent(event.room_id, event.replaces_state)).content.membership;
-                }
-                await this.discord.HandleMatrixKickBan(
-                    event.room_id,
-                    event.state_key,
-                    event.sender,
-                    event.content!.membership as "leave"|"ban",
-                    prevMembership,
-                    event.content!.reason,
-                );
-            }
-            return;
-        } else if (["m.room.member", "m.room.name", "m.room.topic"].includes(event.type)) {
-            await this.discord.ProcessMatrixStateEvent(event);
-            return;
-        } else if (event.type === "m.room.redaction" && context.rooms.remote) {
-            await this.discord.ProcessMatrixRedact(event);
-            return;
-        } else if (event.type === "m.room.message" || event.type === "m.sticker") {
-            log.verbose(`Got ${event.type} event`);
-            const isBotCommand = event.type === "m.room.message" &&
-                event.content!.body &&
-                event.content!.body!.startsWith("!discord");
-            if (isBotCommand) {
-                await this.ProcessCommand(event, context);
-                return;
-            } else if (context.rooms.remote) {
-                const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS);
-                try {
-                    await this.discord.ProcessMatrixMsgEvent(event, srvChanPair[0], srvChanPair[1]);
-                    return;
-                } catch (err) {
-                    log.warn("There was an error sending a matrix event", err);
-                    return;
-                }
-            }
-        } else if (event.type === "m.room.encryption" && context.rooms.remote) {
-            try {
-                await this.HandleEncryptionWarning(event.room_id);
-                return;
-            } catch (err) {
-                throw new Error(`Failed to handle encrypted room, ${err}`);
-            }
-        } else {
-            log.verbose("Got non m.room.message event");
-        }
-        log.verbose("Event not processed by bridge");
-    }
-
-    public async HandleEncryptionWarning(roomId: string): Promise<void> {
-        const intent = this.bridge.getIntent();
-        log.info(`User has turned on encryption in ${roomId}, so leaving.`);
-        /* N.B 'status' is not specced but https://github.com/matrix-org/matrix-doc/pull/828
-         has been open for over a year with no resolution. */
-        const sendPromise = intent.sendMessage(roomId, {
-            body: "You have turned on encryption in this room, so the service will not bridge any new messages.",
-            msgtype: "m.notice",
-            status: "critical",
-        });
-        const channel = await this.discord.GetChannelFromRoomId(roomId);
-        await (channel as Discord.TextChannel).send(
-          "Someone on Matrix has turned on encryption in this room, so the service will not bridge any new messages",
-        );
-        await sendPromise;
-        await intent.leave(roomId);
-        await this.roomStore.removeEntriesByMatrixRoomId(roomId);
-    }
-
-    public async HandleInvite(event: IMatrixEvent) {
-        log.info(`Received invite for ${event.state_key} in room ${event.room_id}`);
-        if (event.state_key === this.botUserId) {
-            log.info("Accepting invite for bridge bot");
-            await this.joinRoom(this.bridge.getIntent(), event.room_id);
-            this.botJoinedRooms.add(event.room_id);
-        } else {
-            await this.discord.ProcessMatrixStateEvent(event);
-        }
-    }
-
-    public async ProcessCommand(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",
-                });
-            }
-
-            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
-        }
-    }
-
     public async OnAliasQuery(alias: string, aliasLocalpart: string): Promise<ProvisionedRoom> {
         log.info("Got request for #", aliasLocalpart);
         const srvChanPair = aliasLocalpart.substr("_discord_".length).split("_", ROOM_NAME_PARTS);
@@ -662,21 +395,4 @@ export class MatrixRoomHandler {
             creationOpts,
         } as ProvisionedRoom;
     }
-
-    private async isBotInRoom(roomId: string): Promise<boolean> {
-        // Update the room cache, if not done already.
-        if (Date.now () - this.botJoinedRoomsCacheUpdatedAt > ROOM_CACHE_MAXAGE_MS) {
-            log.verbose("Updating room cache for bot...");
-            try {
-                log.verbose("Got new room cache for bot");
-                this.botJoinedRoomsCacheUpdatedAt = Date.now();
-                const rooms = (await this.bridge.getBot().getJoinedRooms()) as string[];
-                this.botJoinedRooms = new Set(rooms);
-            } catch (e) {
-                log.error("Failed to get room cache for bot, ", e);
-                return false;
-            }
-        }
-        return this.botJoinedRooms.has(roomId);
-    }
 }
diff --git a/test/test_matrixcommandhandler.ts b/test/test_matrixcommandhandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8bef1b1462b331713d18f43a834cc69eb97587b0
--- /dev/null
+++ b/test/test_matrixcommandhandler.ts
@@ -0,0 +1,326 @@
+import * as Chai from "chai";
+import { MatrixCommandHandler } from "../src/matrixcommandhandler";
+import { DiscordBridgeConfig } from "../src/config";
+import { MockChannel } from "./mocks/channel";
+
+// 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 = {};
+
+function createCH(opts: any = {}) {
+    USERSJOINED = 0;
+    USERSKICKED = 0;
+    USERSBANNED = 0;
+    USERSUNBANNED = 0;
+    MESSAGESENT = {};
+
+    const bridge = {
+        getBot: () => {
+            return {
+                getJoinedRooms: () => ["!123:localhost"],
+                isRemoteUser: (id) => {
+                    return id !== undefined && id.startsWith("@_discord_");
+                },
+            };
+        },
+        getIntent: () => {
+            return {
+                ban: async () => { USERSBANNED++; },
+                getClient: () => mxClient,
+                join: () => { USERSJOINED++; },
+                joinRoom: async () => { USERSJOINED++; },
+                kick: async () => { USERSKICKED++; },
+                leave: () => { },
+                sendMessage: async (roomId, content) => { MESSAGESENT = content; return content; },
+                unban: async () => { USERSUNBANNED++; },
+            };
+        },
+    };
+
+    const config = new DiscordBridgeConfig();
+    config.limits.roomGhostJoinDelay = 0;
+    if (opts.disableSS) {
+        config.bridge.enableSelfServiceBridging = false;
+    } else {
+        config.bridge.enableSelfServiceBridging = true;
+    }
+    const mxClient = {
+        getStateEvent: async () => {
+            return opts.powerLevels || {};
+        },
+        getUserId: () => "@user:localhost",
+        joinRoom: async () => {
+            USERSJOINED++;
+        },
+        sendReadReceipt: async () => { },
+        setRoomDirectoryVisibilityAppService: async () => { },
+    };
+    const provisioner = {
+        AskBridgePermission: async () => {
+            if (opts.denyBridgePermission) {
+                throw new Error("The bridge has been declined by the Discord guild");
+            }
+        },
+        BridgeMatrixRoom: () => {
+            if (opts.failBridgeMatrix) {
+                throw new Error("Test failed matrix bridge");
+            }
+        },
+        UnbridgeRoom: async () => {
+            if (opts.failUnbridge) {
+                throw new Error("Test failed unbridge");
+            }
+        },
+    };
+    const bot = {
+        GetBotId: () => "@botuser:localhost",
+        LookupRoom: async (guildid, discordid) => {
+            if (guildid !== "123") {
+                throw new Error("Guild not found");
+            } else if (discordid !== "456") {
+                throw new Error("Channel not found");
+            }
+            const channel = new MockChannel();
+            return {channel, botUser: true };
+        },
+        Provisioner: provisioner,
+    };
+    return new MatrixCommandHandler(bot as any, bridge, config);
+}
+
+describe("MatrixCommandHandler", () => {
+    describe("ProcessCommand", () => {
+        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;
+        });
+        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;
+        });
+        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",
+            });
+            expect(evt.body.startsWith("Available commands")).to.be.true;
+        });
+        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");
+            });
+            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");
+            });
+            it("will fail to bridge if permissions were denied", 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 " +
+                    "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.");
+            });
+            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");
+            });
+            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");
+            });
+        });
+        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");
+            });
+            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.");
+            });
+            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.");
+            });
+            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.");
+            });
+        });
+    });
+    describe("HandleInvite", () => {
+        it("should accept invite for bot user", async () => {
+            const handler: any = createCH();
+            let joinedRoom = false;
+            handler.joinRoom = async () => {
+                joinedRoom = true;
+            };
+            await handler.HandleInvite({
+                state_key: "@botuser:localhost",
+            });
+            expect(USERSJOINED).to.equal(1);
+        });
+        it("should deny invite for other users", async () => {
+            const handler: any = createCH();
+            let joinedRoom = false;
+            handler.joinRoom = async () => {
+                joinedRoom = true;
+            };
+            await handler.HandleInvite({
+                state_key: "@user:localhost",
+            });
+            expect(joinedRoom).to.be.false;
+        });
+    });
+});
diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts
index f0b718543d5f0f05aa5c8d8fb0a5b875bbc006ac..71ec84bf415758f4abb0e4b1e8a7f234c261802f 100644
--- a/test/test_matrixeventprocessor.ts
+++ b/test/test_matrixeventprocessor.ts
@@ -36,6 +36,14 @@ const TEST_TIMESTAMP = 1337;
 
 const expect = Chai.expect;
 // const assert = Chai.assert;
+function buildRequest(eventData) {
+    if (eventData.unsigned === undefined) {
+        eventData.unsigned = {age: 0};
+    }
+    return {
+        getData: () => eventData,
+    };
+}
 const bot = {
     GetIntentFromDiscordMember: (member) => {
         return {
@@ -77,7 +85,16 @@ const mxClient = {
     },
 };
 
+let STATE_EVENT_MSG = "";
+let USERSYNC_HANDLED = false;
+let MESSAGE_PROCCESS = "";
+let KICKBAN_HANDLED = false;
+
 function createMatrixEventProcessor(): MatrixEventProcessor {
+    USERSYNC_HANDLED = false;
+    STATE_EVENT_MSG = "";
+    MESSAGE_PROCCESS = "";
+    KICKBAN_HANDLED = true;
     const bridge = {
         getBot: () => {
             return {
@@ -159,7 +176,9 @@ function createMatrixEventProcessor(): MatrixEventProcessor {
                             sender: "@fox:localhost",
                         };
                     }
-                    return null;
+                    return {
+                        content: {},
+                    };
                 },
                 getProfileInfo: async (userId: string) => {
                     if (userId !== "@doggo:localhost") {
@@ -173,6 +192,12 @@ function createMatrixEventProcessor(): MatrixEventProcessor {
             };
         },
     };
+    const us = {
+        OnMemberState: async () => {
+            USERSYNC_HANDLED = true;
+        },
+        OnUpdateUser: async () => { },
+    };
     const config = new DiscordBridgeConfig();
 
     const Util = Object.assign(require("../src/util").Util, {
@@ -182,11 +207,34 @@ function createMatrixEventProcessor(): MatrixEventProcessor {
         },
     });
     const discordbot = {
+        GetBotId: () => "@botuser:localhost",
+        GetChannelFromRoomId: async (roomId) => {
+            return new MockChannel("123456");
+        },
         GetDiscordUserOrMember: async (s) => {
             return new Discord.User({ } as any, { username: "Someuser" });
         },
+        HandleMatrixKickBan: () => {
+            KICKBAN_HANDLED = true;
+        },
+        ProcessMatrixRedact: async (evt) => {
+            MESSAGE_PROCCESS = "redacted";
+        },
+        UserSyncroniser: us,
+        sendAsBot: async (msg, channel, event) => {
+            STATE_EVENT_MSG = msg;
+        },
     };
 
+    const ch = Object.assign(new (require("../src/matrixcommandhandler").MatrixCommandHandler)(bot as any, config), {
+        HandleInvite: async (evt) => {
+            MESSAGE_PROCCESS = "invited";
+        },
+        ProcessCommand: async (evt) => {
+            MESSAGE_PROCCESS = "command_processed";
+        },
+    });
+
     return new (Proxyquire("../src/matrixeventprocessor", {
         "./util": {
             Util,
@@ -196,34 +244,33 @@ function createMatrixEventProcessor(): MatrixEventProcessor {
             config,
             bridge,
             discordbot as any,
-    ));
+    ), ch);
 }
 const mockChannel = new MockChannel();
 mockChannel.members.set("12345", new MockMember("12345", "testuser2"));
 
 describe("MatrixEventProcessor", () => {
-    describe("StateEventToMessage", () => {
-        it("Should ignore unhandled states", () => {
+    describe("ProcessStateEvent", () => {
+        it("Should ignore unhandled states", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
+                room_id: "!someroom:localhost",
                 sender: "@user:localhost",
                 type: "m.room.nonexistant",
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, undefined);
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("");
         });
-        it("Should ignore bot user states", () => {
+        it("Should ignore bot user states", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 sender: "@botuser:localhost",
                 type: "m.room.member",
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, undefined);
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("");
         });
-        it("Should echo name changes", () => {
+        it("Should echo name changes", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 content: {
@@ -232,11 +279,10 @@ describe("MatrixEventProcessor", () => {
                 sender: "@user:localhost",
                 type: "m.room.name",
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, "`@user:localhost` set the name to `Test Name` on Matrix.");
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("`@user:localhost` set the name to `Test Name` on Matrix.");
         });
-        it("Should echo topic changes", () => {
+        it("Should echo topic changes", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 content: {
@@ -245,11 +291,10 @@ describe("MatrixEventProcessor", () => {
                 sender: "@user:localhost",
                 type: "m.room.topic",
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, "`@user:localhost` set the topic to `Test Topic` on Matrix.");
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("`@user:localhost` set the topic to `Test Topic` on Matrix.");
         });
-        it("Should echo joins", () => {
+        it("Should echo joins", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 content: {
@@ -259,11 +304,10 @@ describe("MatrixEventProcessor", () => {
                 type: "m.room.member",
                 unsigned: {},
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, "`@user:localhost` joined the room on Matrix.");
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("`@user:localhost` joined the room on Matrix.");
         });
-        it("Should echo invites", () => {
+        it("Should echo invites", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 content: {
@@ -274,11 +318,10 @@ describe("MatrixEventProcessor", () => {
                 type: "m.room.member",
                 unsigned: {},
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, "`@user:localhost` invited `@user2:localhost` to the room on Matrix.");
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("`@user:localhost` invited `@user2:localhost` to the room on Matrix.");
         });
-        it("Should echo kicks", () => {
+        it("Should echo kicks", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 content: {
@@ -289,11 +332,10 @@ describe("MatrixEventProcessor", () => {
                 type: "m.room.member",
                 unsigned: {},
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, "`@user:localhost` kicked `@user2:localhost` from the room on Matrix.");
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("`@user:localhost` kicked `@user2:localhost` from the room on Matrix.");
         });
-        it("Should echo leaves", () => {
+        it("Should echo leaves", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 content: {
@@ -304,11 +346,10 @@ describe("MatrixEventProcessor", () => {
                 type: "m.room.member",
                 unsigned: {},
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, "`@user:localhost` left the room on Matrix.");
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("`@user:localhost` left the room on Matrix.");
         });
-        it("Should echo bans", () => {
+        it("Should echo bans", async () => {
             const processor = createMatrixEventProcessor();
             const event = {
                 content: {
@@ -319,9 +360,8 @@ describe("MatrixEventProcessor", () => {
                 type: "m.room.member",
                 unsigned: {},
             } as IMatrixEvent;
-            const channel = new MockChannel("123456");
-            const msg = processor.StateEventToMessage(event, channel as any);
-            Chai.assert.equal(msg, "`@user:localhost` banned `@user2:localhost` from the room on Matrix.");
+            await processor.ProcessStateEvent(event);
+            expect(STATE_EVENT_MSG).to.equal("`@user:localhost` banned `@user2:localhost` from the room on Matrix.");
         });
     });
     describe("EventToEmbed", () => {
@@ -767,4 +807,165 @@ This is the reply`,
             expect(result!.description).to.be.equal("[package.zip](https://package/localhost)");
         });
     });
+    describe("OnEvent", () => {
+        it("should reject old events", async () => {
+            const AGE = 900001; // 15 * 60 * 1000
+            const processor = createMatrixEventProcessor();
+            await processor.OnEvent(buildRequest({unsigned: {age: AGE}}), null);
+            expect(MESSAGE_PROCCESS).equals("");
+        });
+        it("should reject un-processable events", async () => {
+            const AGE = 900000; // 15 * 60 * 1000
+            const processor = createMatrixEventProcessor();
+            // check if nothing is thrown
+            await processor.OnEvent(buildRequest({
+                content: {},
+                type: "m.potato",
+                unsigned: {age: AGE}}), null);
+            expect(MESSAGE_PROCCESS).equals("");
+        });
+        it("should handle own invites", async () => {
+            const processor = createMatrixEventProcessor();
+            await processor.OnEvent(buildRequest({
+                content: {membership: "invite"},
+                state_key: "@botuser:localhost",
+                type: "m.room.member"}), null);
+            expect(MESSAGE_PROCCESS).to.equal("invited");
+        });
+        it("should handle kicks to own members", async () => {
+            const processor = createMatrixEventProcessor();
+            await processor.OnEvent(buildRequest({
+                content: {membership: "leave"},
+                sender: "@badboy:localhost",
+                state_key: "@_discord_12345:localhost",
+                type: "m.room.member"}), null);
+            expect(KICKBAN_HANDLED).to.be.true;
+        });
+        it("should handle bans to own members", async () => {
+            const processor = createMatrixEventProcessor();
+            await processor.OnEvent(buildRequest({
+                content: {membership: "ban"},
+                sender: "@badboy:localhost",
+                state_key: "@_discord_12345:localhost",
+                type: "m.room.member"}), null);
+            expect(KICKBAN_HANDLED).to.be.true;
+        });
+        it("should pass other member types to state event", async () => {
+            const processor = createMatrixEventProcessor();
+            let stateevent = false;
+            processor.ProcessStateEvent = async (ev) => {
+                stateevent = true;
+            };
+            await processor.OnEvent(buildRequest({
+                content: {membership: "join"},
+                state_key: "@bacon:localhost",
+                type: "m.room.member"}), null);
+            expect(MESSAGE_PROCCESS).to.equal("");
+            expect(stateevent).to.be.true;
+        });
+        it("should handle redactions with existing rooms", async () => {
+            const processor = createMatrixEventProcessor();
+            const context = {
+                rooms: {
+                    remote: true,
+                },
+            };
+            await processor.OnEvent(buildRequest({
+                type: "m.room.redaction"}), context);
+            expect(MESSAGE_PROCCESS).equals("redacted");
+        });
+        it("should ignore redactions with no linked room", async () => {
+            const processor = createMatrixEventProcessor();
+            const context = {
+                rooms: {
+                    remote: null,
+                },
+            };
+            await processor.OnEvent(buildRequest({
+                    type: "m.room.redaction"}), context);
+            expect(MESSAGE_PROCCESS).equals("");
+        });
+        it("should process regular messages", async () => {
+            const processor = createMatrixEventProcessor();
+            const context = {
+                rooms: {
+                    remote: {
+                        roomId: "_discord_123_456",
+                    },
+                },
+            };
+            let processed = false;
+            processor.ProcessMsgEvent = async (evt, _, __) => {
+                processed = true;
+            };
+            await processor.OnEvent(buildRequest({
+                content: {body: "abc"},
+                type: "m.room.message",
+            }), context);
+            expect(MESSAGE_PROCCESS).to.equal("");
+            expect(processed).to.be.true;
+        });
+        it("should alert if encryption is turned on", async () => {
+            const processor = createMatrixEventProcessor();
+            const context = {
+                rooms: {
+                    remote: {
+                        roomId: "_discord_123_456",
+                    },
+                },
+            };
+            let encrypt = false;
+            processor.HandleEncryptionWarning = async (evt) => {
+                encrypt = true;
+            };
+            await processor.OnEvent(buildRequest({
+                room_id: "!accept:localhost",
+                type: "m.room.encryption",
+            }), context);
+            expect(encrypt).to.be.true;
+        });
+        it("should process !discord commands", async () => {
+            const processor = createMatrixEventProcessor();
+            await processor.OnEvent(buildRequest({
+                content: {body: "!discord cmd"},
+                type: "m.room.message",
+            }), null);
+            expect(MESSAGE_PROCCESS).to.equal("command_processed");
+        });
+        it("should ignore regular messages with no linked room", async () => {
+            const processor = createMatrixEventProcessor();
+            const context = {
+                rooms: {
+                    remote: null,
+                },
+            };
+            await processor.OnEvent(buildRequest({
+                content: {body: "abc"},
+                type: "m.room.message",
+            }), context);
+            expect(MESSAGE_PROCCESS).equals("");
+        });
+        it("should process stickers", async () => {
+            const processor = createMatrixEventProcessor();
+            const context = {
+                rooms: {
+                    remote: {
+                        roomId: "_discord_123_456",
+                    },
+                },
+            };
+            let processed = false;
+            processor.ProcessMsgEvent = async (evt, _, __) => {
+                processed = true;
+            };
+            await processor.OnEvent(buildRequest({
+                content: {
+                    body: "abc",
+                    url: "mxc://abc",
+                },
+                type: "m.sticker",
+            }), context);
+            expect(processed).to.be.true;
+        });
+    });
 });
diff --git a/test/test_matrixroomhandler.ts b/test/test_matrixroomhandler.ts
index d653c7c1ecff472d527bdeb1e6430aa5741729f1..4c5de12bb552fc923823a55044857f3531b394ad 100644
--- a/test/test_matrixroomhandler.ts
+++ b/test/test_matrixroomhandler.ts
@@ -49,15 +49,6 @@ let USERSYNC_HANDLED = false;
 let KICKBAN_HANDLED = false;
 let MESSAGE_PROCCESS = "";
 
-function buildRequest(eventData) {
-    if (eventData.unsigned === undefined) {
-        eventData.unsigned = {age: 0};
-    }
-    return {
-        getData: () => eventData,
-    };
-}
-
 function createRH(opts: any = {}) {
     USERSJOINED = 0;
     USERSKICKED = 0;
@@ -225,387 +216,6 @@ describe("MatrixRoomHandler", () => {
             }
         });
     });
-    describe("OnEvent", () => {
-        it("should reject old events", async () => {
-            const AGE = 900001; // 15 * 60 * 1000
-            const handler = createRH();
-            await handler.OnEvent(buildRequest({unsigned: {age: AGE}}), null);
-            expect(MESSAGE_PROCCESS).equals("");
-        });
-        it("should reject un-processable events", async () => {
-            const AGE = 900000; // 15 * 60 * 1000
-            const handler = createRH();
-            // check if nothing is thrown
-            await handler.OnEvent(buildRequest({
-                content: {},
-                type: "m.potato",
-                unsigned: {age: AGE}}), null);
-            expect(MESSAGE_PROCCESS).equals("");
-        });
-        it("should handle invites", async () => {
-            const handler = createRH();
-            let invited = false;
-            handler.HandleInvite = async (ev) => {
-                invited = true;
-            };
-            await handler.OnEvent(buildRequest({
-                content: {membership: "invite"},
-                type: "m.room.member"}), null);
-            expect(invited).to.be.true;
-        });
-        it("should handle kicks to own members", async () => {
-            const handler = createRH();
-            await handler.OnEvent(buildRequest({
-                content: {membership: "leave"},
-                sender: "@badboy:localhost",
-                state_key: "@_discord_12345:localhost",
-                type: "m.room.member"}), null);
-            expect(KICKBAN_HANDLED).to.be.true;
-        });
-        it("should handle bans to own members", async () => {
-            const handler = createRH();
-            await handler.OnEvent(buildRequest({
-                content: {membership: "ban"},
-                sender: "@badboy:localhost",
-                state_key: "@_discord_12345:localhost",
-                type: "m.room.member"}), null);
-            expect(KICKBAN_HANDLED).to.be.true;
-        });
-        it("should pass other member types to state event", async () => {
-            const handler = createRH();
-            let invited = false;
-            handler.HandleInvite = async (ev) => {
-                invited = true;
-            };
-            handler.OnEvent(buildRequest({
-                content: {membership: "join"},
-                state_key: "@bacon:localhost",
-                type: "m.room.member"}), null);
-            expect(invited).to.be.false;
-            expect(MESSAGE_PROCCESS).equals("stateevent");
-        });
-        it("should handle redactions with existing rooms", async () => {
-            const handler = createRH();
-            const context = {
-                rooms: {
-                    remote: true,
-                },
-            };
-            await handler.OnEvent(buildRequest({
-                type: "m.room.redaction"}), context);
-            expect(MESSAGE_PROCCESS).equals("redacted");
-        });
-        it("should ignore redactions with no linked room", async () => {
-            const handler = createRH();
-            const context = {
-                rooms: {
-                    remote: null,
-                },
-            };
-            await handler.OnEvent(buildRequest({
-                    type: "m.room.redaction"}), context);
-            expect(MESSAGE_PROCCESS).equals("");
-        });
-        it("should process regular messages", async () => {
-            const handler = createRH();
-            const context = {
-                rooms: {
-                    remote: {
-                        roomId: "_discord_123_456",
-                    },
-                },
-            };
-            await handler.OnEvent(buildRequest({
-                content: {body: "abc"},
-                type: "m.room.message",
-            }), context);
-            expect(MESSAGE_PROCCESS).equals("processed");
-        });
-        it("should alert if encryption is turned on", async () => {
-            const handler = createRH();
-            const context = {
-                rooms: {
-                    remote: {
-                        roomId: "_discord_123_456",
-                    },
-                },
-            };
-            await handler.OnEvent(buildRequest({
-                room_id: "!accept:localhost",
-                type: "m.room.encryption",
-            }), context);
-        });
-        it("should process !discord commands", async () => {
-            const handler = createRH();
-            let processedcmd = false;
-            handler.ProcessCommand = async (ev) => {
-                processedcmd = true;
-            };
-            await handler.OnEvent(buildRequest({
-                content: {body: "!discord cmd"},
-                type: "m.room.message",
-            }), null);
-            expect(processedcmd).to.be.true;
-        });
-        it("should ignore regular messages with no linked room", async () => {
-            const handler = createRH();
-            const context = {
-                rooms: {
-                    remote: null,
-                },
-            };
-            await handler.OnEvent(buildRequest({
-                content: {body: "abc"},
-                type: "m.room.message",
-            }), context);
-            expect(MESSAGE_PROCCESS).equals("");
-        });
-        it("should process stickers", async () => {
-            const handler = createRH();
-            const context = {
-                rooms: {
-                    remote: {
-                        roomId: "_discord_123_456",
-                    },
-                },
-            };
-            await handler.OnEvent(buildRequest({
-                content: {
-                    body: "abc",
-                    url: "mxc://abc",
-                },
-                type: "m.sticker",
-            }), context);
-            expect(MESSAGE_PROCCESS).equals("processed");
-        });
-    });
-    describe("HandleInvite", () => {
-        it("should accept invite for bot user", async () => {
-            const handler: any = createRH();
-            let joinedRoom = false;
-            handler.joinRoom = async () => {
-                joinedRoom = true;
-            };
-            await handler.HandleInvite({
-                state_key: "@botuser:localhost",
-            });
-            expect(joinedRoom).to.be.true;
-        });
-        it("should deny invite for other users", async () => {
-            const handler: any = createRH();
-            let joinedRoom = false;
-            handler.joinRoom = async () => {
-                joinedRoom = true;
-            };
-            await handler.HandleInvite({
-                state_key: "@user:localhost",
-            });
-            expect(joinedRoom).to.be.false;
-        });
-    });
-    describe("ProcessCommand", () => {
-        it("should not process command if not in room", async () => {
-            const handler: any = createRH({disableSS: true});
-            const ret = await handler.ProcessCommand({
-                room_id: "!666:localhost",
-            });
-            expect(ret).to.be.undefined;
-        });
-        it("should warn if self service is disabled", async () => {
-            const handler: any = createRH({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 = createRH();
-            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 = createRH({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 = createRH({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;
-        });
-        it("should allow if user is powerful enough with their own state", async () => {
-            const handler: any = createRH({powerLevels: {
-                users: {
-                 "@user:localhost": 100,
-                },
-            }});
-            const evt = await handler.ProcessCommand({
-                content: {body: "!discord help"},
-                room_id: "!123:localhost",
-                sender: "@user:localhost",
-            });
-            expect(evt.body.startsWith("Available commands")).to.be.true;
-        });
-        describe("!discord bridge", () => {
-            it("will bridge a new room, and ask for permissions", async () => {
-                const handler: any = createRH({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");
-            });
-            it("will fail to bridge if permissions were denied", async () => {
-                const handler: any = createRH({
-                    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");
-            });
-            it("will fail to bridge if permissions were denied", async () => {
-                const handler: any = createRH({
-                    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 " +
-                    "the guild owner approved the bridge?");
-            });
-            it("will not bridge if a link already exists", async () => {
-                const handler: any = createRH({
-                    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.");
-            });
-            it("will not bridge without required args", async () => {
-                const handler: any = createRH({
-                    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");
-            });
-            it("will bridge with x/y syntax", async () => {
-                const handler: any = createRH({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");
-            });
-        });
-        describe("!discord unbridge", () => {
-            it("will unbridge", async () => {
-                const handler: any = createRH({
-                    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");
-            });
-            it("will not unbridge if a link does not exist", async () => {
-                const handler: any = createRH({
-                    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.");
-            });
-            it("will not unbridge non-plumbed rooms", async () => {
-                const handler: any = createRH({
-                    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.");
-            });
-            it("will show error if unbridge fails", async () => {
-                const handler: any = createRH({
-                    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.");
-            });
-        });
-    });
     describe("OnAliasQuery", () => {
         it("will create room", async () => {
             const handler: any = createRH({});