diff --git a/config/config.sample.yaml b/config/config.sample.yaml
index 925df2bbadf7195dd2049b2c01c901c61620e0fa..146668def098cd7a639b253fb4cf49030c6ea3b3 100644
--- a/config/config.sample.yaml
+++ b/config/config.sample.yaml
@@ -19,20 +19,11 @@ bridge:
   disablePresence: false
   # Disable sending typing notifications when somebody on Discord types.
   disableTypingNotifications: false
-  # Disable parsing discord usernames out of matrix messages so
-  # that it highlights discord users.
-  # WARNING: Not always 100% accurate, but close enough usually.
-  disableDiscordMentions: false
   # Disable deleting messages on Discord if a message is redacted on Matrix.
   disableDeletionForwarding: false
   # Enable users to bridge rooms using !discord commands. See
   # https://t2bot.io/discord for instructions.
   enableSelfServiceBridging: false
-  # For both below, a space is inserted after @ to stop the mentions working.
-  # Disable relaying @everyone to Discord. Non-puppeted users can abuse this.
-  disableEveryoneMention: false
-  # Disable relaying @here to Discord. Non-puppeted users can abuse this.
-  disableHereMention: false
 # Authentication configuration for the discord bot.
 auth:
   clientID: "12345"
diff --git a/config/config.schema.yaml b/config/config.schema.yaml
index 4f832fa6ce069b1a2560c318a806e09c9127b00a..17b816ca6060f16f7bd0cd4e6872ce2c9d102a08 100644
--- a/config/config.schema.yaml
+++ b/config/config.schema.yaml
@@ -16,16 +16,10 @@ properties:
             type: "boolean"
           disableTypingNotifications:
             type: "boolean"
-          disableDiscordMentions:
-            type: "boolean"
           disableDeletionForwarding:
             type: "boolean"
           enableSelfServiceBridging:
             type: "boolean"
-          disableEveryoneMention:
-            type: "boolean"
-          disableHereMention:
-            type: "boolean"
     auth:
         type: "object"
         required: ["botToken", "clientID"]
diff --git a/package-lock.json b/package-lock.json
index bbd3538fe25e7f5a4d5c0e7e2806b16136b105f5..eb94d0b206ddd9af8befd9779390042aed5e3557 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1375,8 +1375,7 @@
     "he": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
-      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
-      "dev": true
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
     },
     "highlight.js": {
       "version": "9.13.1",
@@ -2064,6 +2063,14 @@
       "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
       "dev": true
     },
+    "node-html-parser": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.11.tgz",
+      "integrity": "sha512-KOjvmbk0yWuy/cN8uqk6bVYS0Lue+jVWcLO/zmnCtz8FPXhj00apBN376FoM6QmFMMbJwXQdKf5ko6G1S6bnrw==",
+      "requires": {
+        "he": "1.1.1"
+      }
+    },
     "nopt": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
diff --git a/package.json b/package.json
index 70430f21574548a41aca058155c92071f4928818..9a3b0a85cf7687f54f382c9d1332872d872db860 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "matrix-appservice-bridge": "^1.7.0",
     "mime": "^1.6.0",
     "moment": "^2.22.2",
+    "node-html-parser": "^1.1.11",
     "pg-promise": "^8.5.1",
     "tslint": "^5.11.0",
     "typescript": "^3.1.3",
diff --git a/src/bot.ts b/src/bot.ts
index 5c651649edcf803922acb10b10bb26d23da3448d..c76df58a4f8d65a0eec5d452aee06d11717304c7 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -5,7 +5,11 @@ import { DbEmoji } from "./db/dbdataemoji";
 import { DbEvent } from "./db/dbdataevent";
 import { MatrixUser, RemoteUser, Bridge, Entry, Intent } from "matrix-appservice-bridge";
 import { Util } from "./util";
-import { MessageProcessor, MessageProcessorOpts, MessageProcessorMatrixResult } from "./messageprocessor";
+import {
+    DiscordMessageProcessor,
+    DiscordMessageProcessorOpts,
+    DiscordMessageProcessorResult,
+} from "./discordmessageprocessor";
 import { MatrixEventProcessor, MatrixEventProcessorOpts } from "./matrixeventprocessor";
 import { PresenceHandler } from "./presencehandler";
 import { Provisioner } from "./provisioner";
@@ -49,7 +53,7 @@ export class DiscordBot {
     private bridge: Bridge;
     private presenceInterval: number;
     private sentMessages: string[];
-    private msgProcessor: MessageProcessor;
+    private discordMsgProcessor: DiscordMessageProcessor;
     private mxEventProcessor: MatrixEventProcessor;
     private presenceHandler: PresenceHandler;
     private userSync: UserSyncroniser;
@@ -64,8 +68,8 @@ export class DiscordBot {
         this.store = store;
         this.sentMessages = [];
         this.clientFactory = new DiscordClientFactory(store, config.auth);
-        this.msgProcessor = new MessageProcessor(
-            new MessageProcessorOpts(this.config.bridge.domain, this),
+        this.discordMsgProcessor = new DiscordMessageProcessor(
+            new DiscordMessageProcessorOpts(this.config.bridge.domain, this),
         );
         this.presenceHandler = new PresenceHandler(this);
         this.discordMessageQueue = {};
@@ -325,15 +329,8 @@ export class DiscordBot {
         const result = await this.LookupRoom(guildId, channelId, event.sender);
         const chan = result.channel;
         const botUser = result.botUser;
-        let profile = null;
-        if (result.botUser) {
-            // We are doing this through webhooks so fetch the user profile.
-            profile = await mxClient.getStateEvent(event.room_id, "m.room.member", event.sender);
-            if (profile === null) {
-                log.warn(`User ${event.sender} has no member state. That's odd.`);
-            }
-        }
-        const embedSet = await this.mxEventProcessor.EventToEmbed(event, profile, chan);
+
+        const embedSet = await this.mxEventProcessor.EventToEmbed(event, chan);
         const embed = embedSet.messageEmbed;
         const opts: Discord.MessageOptions = {};
         const file = await this.mxEventProcessor.HandleAttachment(event, mxClient);
@@ -516,7 +513,15 @@ export class DiscordBot {
         }
     }
 
-    private async SendMatrixMessage(matrixMsg: MessageProcessorMatrixResult, chan: Discord.Channel,
+    public async GetEmojiByMxc(mxc: string): Promise<DbEmoji> {
+        const dbEmoji = await this.store.Get(DbEmoji, {mxc_url: mxc});
+        if (!dbEmoji || !dbEmoji.Result) {
+            throw new Error("Couldn't fetch from store");
+        }
+        return dbEmoji;
+    }
+
+    private async SendMatrixMessage(matrixMsg: DiscordMessageProcessorResult, chan: Discord.Channel,
                                     guild: Discord.Guild, author: Discord.User,
                                     msgID: string): Promise<boolean> {
         const rooms = await this.channelSync.GetRoomIdsFromChannel(chan);
@@ -654,7 +659,7 @@ export class DiscordBot {
             if (msg.content === null) {
                 return;
             }
-            const result = await this.msgProcessor.FormatDiscordMessage(msg);
+            const result = await this.discordMsgProcessor.FormatMessage(msg);
             if (!result.body) {
                 return;
             }
@@ -699,7 +704,7 @@ export class DiscordBot {
         }
 
         // Create a new edit message using the old and new message contents
-        const editedMsg = await this.msgProcessor.FormatEdit(oldMsg, newMsg);
+        const editedMsg = await this.discordMsgProcessor.FormatEdit(oldMsg, newMsg);
 
         // Send the message to all bridged matrix rooms
         if (!await this.SendMatrixMessage(editedMsg, newMsg.channel, newMsg.guild, newMsg.author, newMsg.id)) {
diff --git a/src/db/dbdataemoji.ts b/src/db/dbdataemoji.ts
index f816c01f1661534fcd31784846fe28faa223f6b6..0813ccc00fb5d752e27a7f09c174220a6476c4d8 100644
--- a/src/db/dbdataemoji.ts
+++ b/src/db/dbdataemoji.ts
@@ -12,11 +12,19 @@ export class DbEmoji implements IDbData {
     public Result: boolean;
 
     public async RunQuery(store: DiscordStore, params: ISqlCommandParameters): Promise<void> {
-        const row = await store.db.Get(`
+        let query = `
             SELECT *
             FROM emoji
-            WHERE emoji_id = $id`, {
+            WHERE emoji_id = $id`;
+        if (params.mxc_url) {
+            query = `
+                SELECT *
+                FROM emoji
+                WHERE mxc_url = $mxc`;
+        }
+        const row = await store.db.Get(query, {
                 id: params.emoji_id,
+                mxc: params.mxc_url,
             });
         this.Result = row !== undefined;
         if (this.Result) {
diff --git a/src/messageprocessor.ts b/src/discordmessageprocessor.ts
similarity index 92%
rename from src/messageprocessor.ts
rename to src/discordmessageprocessor.ts
index 20ab5828a6748f2b2b95eab7aeba6dc4b9af821b..bddd2cfe748ffcaa9e287a546f64c71dd4315014 100644
--- a/src/messageprocessor.ts
+++ b/src/discordmessageprocessor.ts
@@ -5,7 +5,7 @@ import * as escapeHtml from "escape-html";
 import { Util } from "./util";
 
 import { Log } from "./log";
-const log = new Log("MessageProcessor");
+const log = new Log("DiscordMessageProcessor");
 
 const MATRIX_TO_LINK = "https://matrix.to/#/";
 const MXC_INSERT_REGEX = /\x01(\w+)\x01([01])\x01([0-9]*)\x01/g;
@@ -14,13 +14,13 @@ const ANIMATED_MXC_INSERT_REGEX_GROUP = 2;
 const ID_MXC_INSERT_REGEX_GROUP = 3;
 const EMOJI_SIZE = 32;
 
-export class MessageProcessorOpts {
+export class DiscordMessageProcessorOpts {
     constructor(readonly domain: string, readonly bot?: DiscordBot) {
 
     }
 }
 
-export class MessageProcessorMatrixResult {
+export class DiscordMessageProcessorResult {
     public formattedBody: string;
     public body: string;
     public msgtype: string;
@@ -35,19 +35,19 @@ interface IEmojiNode extends IDiscordNode {
     name: string;
 }
 
-export class MessageProcessor {
-    private readonly opts: MessageProcessorOpts;
-    constructor(opts: MessageProcessorOpts, bot: DiscordBot | null = null) {
+export class DiscordMessageProcessor {
+    private readonly opts: DiscordMessageProcessorOpts;
+    constructor(opts: DiscordMessageProcessorOpts, bot: DiscordBot | null = null) {
         // Backwards compat
         if (bot !== null) {
-            this.opts = new MessageProcessorOpts(opts.domain, bot);
+            this.opts = new DiscordMessageProcessorOpts(opts.domain, bot);
         } else {
             this.opts = opts;
         }
     }
 
-    public async FormatDiscordMessage(msg: Discord.Message): Promise<MessageProcessorMatrixResult> {
-        const result = new MessageProcessorMatrixResult();
+    public async FormatMessage(msg: Discord.Message): Promise<DiscordMessageProcessorResult> {
+        const result = new DiscordMessageProcessorResult();
 
         let content = msg.content;
 
@@ -76,10 +76,13 @@ export class MessageProcessor {
         return result;
     }
 
-    public async FormatEdit(oldMsg: Discord.Message, newMsg: Discord.Message): Promise<MessageProcessorMatrixResult> {
+    public async FormatEdit(
+        oldMsg: Discord.Message,
+        newMsg: Discord.Message,
+    ): Promise<DiscordMessageProcessorResult> {
         // TODO: Produce a nice, colored diff between the old and new message content
         oldMsg.content = `*edit:* ~~${oldMsg.content}~~ -> ${newMsg.content}`;
-        return this.FormatDiscordMessage(oldMsg);
+        return this.FormatMessage(oldMsg);
     }
 
     public InsertEmbeds(content: string, msg: Discord.Message): string {
diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts
index 8005d0041894d53c93bd1e5c758cd3b67cd57730..2ad0107368de6ebf72a54f592d7917a9a968eca2 100644
--- a/src/matrixeventprocessor.ts
+++ b/src/matrixeventprocessor.ts
@@ -1,5 +1,4 @@
 import * as Discord from "discord.js";
-import { MessageProcessorOpts, MessageProcessor } from "./messageprocessor";
 import { DiscordBot } from "./bot";
 import { DiscordBridgeConfig } from "./config";
 import * as escapeStringRegexp from "escape-string-regexp";
@@ -8,7 +7,8 @@ import * as path from "path";
 import * as mime from "mime";
 import { MatrixUser, Bridge } from "matrix-appservice-bridge";
 import { Client as MatrixClient } from "matrix-js-sdk";
-import { IMatrixEvent, IMatrixEventContent } from "./matrixtypes";
+import { IMatrixEvent, IMatrixEventContent, IMatrixMessage } from "./matrixtypes";
+import { MatrixMessageProcessor, IMatrixMessageProcessorParams } from "./matrixmessageprocessor";
 
 import { Log } from "./log";
 const log = new Log("MatrixEventProcessor");
@@ -16,7 +16,6 @@ const log = new Log("MatrixEventProcessor");
 const MaxFileSize = 8000000;
 const MIN_NAME_LENGTH = 2;
 const MAX_NAME_LENGTH = 32;
-const DISCORD_EMOJI_REGEX = /:(\w+):/g;
 const DISCORD_AVATAR_WIDTH = 128;
 const DISCORD_AVATAR_HEIGHT = 128;
 
@@ -39,11 +38,13 @@ export class MatrixEventProcessor {
     private config: DiscordBridgeConfig;
     private bridge: Bridge;
     private discord: DiscordBot;
+    private matrixMsgProcessor: MatrixMessageProcessor;
 
     constructor(opts: MatrixEventProcessorOpts) {
         this.config = opts.config;
         this.bridge = opts.bridge;
         this.discord = opts.discord;
+        this.matrixMsgProcessor = new MatrixMessageProcessor(this.discord);
     }
 
     public StateEventToMessage(event: IMatrixEvent, channel: Discord.TextChannel): string | undefined {
@@ -85,93 +86,46 @@ export class MatrixEventProcessor {
     }
 
     public async EventToEmbed(
-        event: IMatrixEvent, profile: IMatrixEvent|null, channel: Discord.TextChannel,
+        event: IMatrixEvent, channel: Discord.TextChannel, getReply: boolean = true,
     ): Promise<IMatrixEventProcessorResult> {
-        let body: string = this.config.bridge.disableDiscordMentions ? event.content!.body as string :
-            this.FindMentionsInPlainBody(
-                event.content!.body as string,
-                channel.members.array(),
-            );
-
-        if (event.type === "m.sticker") {
-            body = "";
+        const mxClient = this.bridge.getClientFactory().getClientAs();
+        let profile: IMatrixEvent | null = null;
+        try {
+            profile = await mxClient.getStateEvent(event.room_id, "m.room.member", event.sender);
+            if (!profile) {
+                profile = await mxClient.getProfileInfo(event.sender);
+            }
+            if (!profile) {
+                log.warn(`User ${event.sender} has no member state and no profile. That's odd.`);
+            }
+        } catch (err) {
+            log.warn(`Trying to fetch member state or profile for ${event.sender} failed`, err);
         }
 
-        // Replace @everyone
-        if (this.config.bridge.disableEveryoneMention) {
-            body = body.replace(new RegExp(`@everyone`, "g"), "@ everyone");
+        const params = {
+            mxClient,
+            roomId: event.room_id,
+            userId: event.sender,
+        } as IMatrixMessageProcessorParams;
+        if (profile) {
+            params.displayname = profile.displayname;
         }
 
-        // Replace @here
-        if (this.config.bridge.disableHereMention) {
-            body = body.replace(new RegExp(`@here`, "g"), "@ here");
+        let body: string = "";
+        if (event.type !== "m.sticker") {
+            body = await this.matrixMsgProcessor.FormatMessage(event.content as IMatrixMessage, channel.guild, params);
         }
 
-        /* See issue #82
-        const isMarkdown = (event.content.format === "org.matrix.custom.html");
-        if (!isMarkdown) {
-          body = "\\" + body;
-        }*/
-
-        // Replace /me with * username ...
-        if (event.content!.msgtype === "m.emote") {
-            if (profile &&
-                profile.displayname &&
-                profile.displayname.length >= MIN_NAME_LENGTH &&
-                profile.displayname.length <= MAX_NAME_LENGTH) {
-                body = `*${profile.displayname} ${body}*`;
-            } else {
-                body = `*${body}*`;
-            }
-        }
-
-        // replace <del>blah</del> with ~~blah~~
-        body = body.replace(/<del>([^<]*)<\/del>/g, "~~$1~~");
-
-        // Handle discord custom emoji
-        body = this.ReplaceDiscordEmoji(body, channel.guild);
-
         const messageEmbed = new Discord.RichEmbed();
-        const replyEmbedAndBody = await this.GetEmbedForReply(event);
-        messageEmbed.setDescription(replyEmbedAndBody ? replyEmbedAndBody[1] : body);
+        messageEmbed.setDescription(body);
         await this.SetEmbedAuthor(messageEmbed, event.sender, profile);
+        const replyEmbed = getReply ? (await this.GetEmbedForReply(event, channel)) : undefined;
         return {
             messageEmbed,
-            replyEmbed: replyEmbedAndBody ? replyEmbedAndBody[0] : undefined,
+            replyEmbed,
         };
     }
 
-    public FindMentionsInPlainBody(body: string, members: Discord.GuildMember[]): string {
-        const WORD_BOUNDARY = "(^|\:|\#|```|\\s|$|,)";
-        for (const member of members) {
-            const matcher = `${escapeStringRegexp(`${member.user.username}#${member.user.discriminator}`)}|` +
-                `${escapeStringRegexp(member.displayName)}`;
-            const regex = new RegExp(
-                    `(${WORD_BOUNDARY})(@?(${matcher}))(?=${WORD_BOUNDARY})`
-                    , "igmu");
-
-            body = body.replace(regex, `$1<@!${member.id}>`);
-        }
-        return body;
-    }
-
-    public ReplaceDiscordEmoji(content: string, guild: Discord.Guild): string {
-        let results = DISCORD_EMOJI_REGEX.exec(content);
-        while (results !== null) {
-            const emojiName = results[1];
-            const emojiNameWithColons = results[0];
-
-            // Check if this emoji exists in the guild
-            const emoji = guild.emojis.find((e) => e.name === emojiName);
-            if (emoji) {
-                // Replace :a: with <:a:123ID123>
-                content = content.replace(emojiNameWithColons, `<${emojiNameWithColons}${emoji.id}>`);
-            }
-            results = DISCORD_EMOJI_REGEX.exec(content);
-        }
-        return content;
-    }
-
     public async HandleAttachment(event: IMatrixEvent, mxClient: MatrixClient): Promise<string|Discord.FileOptions> {
         if (!event.content) {
             event.content = {};
@@ -217,46 +171,36 @@ export class MatrixEventProcessor {
         return `[${name}](${url})`;
     }
 
-    public async GetEmbedForReply(event: IMatrixEvent): Promise<[Discord.RichEmbed, string]|undefined> {
+    public async GetEmbedForReply(
+        event: IMatrixEvent,
+        channel: Discord.TextChannel,
+    ): Promise<Discord.RichEmbed|undefined> {
         if (!event.content) {
             event.content = {};
         }
 
         const relatesTo = event.content["m.relates_to"];
-        let eventId = null;
+        let eventId = "";
         if (relatesTo && relatesTo["m.in_reply_to"]) {
             eventId = relatesTo["m.in_reply_to"].event_id;
         } else {
             return;
         }
-        let reponseText = Util.GetReplyFromReplyBody(event.content.body || "");
-        if (reponseText === "") {
-            reponseText = "Reply with unknown content";
-        }
 
         const intent = this.bridge.getIntent();
-        const embed = new Discord.RichEmbed();
         // Try to get the event.
         try {
             const sourceEvent = await intent.getEvent(event.room_id, eventId);
-            let replyText = sourceEvent.content.body  || "Reply with unknown content";
-            // Check if this is also a reply.
-            if (sourceEvent.content && sourceEvent.content["m.relates_to"] &&
-                sourceEvent.content["m.relates_to"]["m.in_reply_to"]) {
-                replyText = Util.GetReplyFromReplyBody(sourceEvent.content.body);
-            }
-            embed.setDescription(replyText);
-            await this.SetEmbedAuthor(
-                embed,
-                sourceEvent.sender,
-            );
+            sourceEvent.content.body = sourceEvent.content.body  || "Reply with unknown content";
+            return (await this.EventToEmbed(sourceEvent, channel, false)).messageEmbed;
         } catch (ex) {
             log.warn("Failed to handle reply, showing a unknown embed:", ex);
-            // For some reason we failed to get the event, so using fallback.
-            embed.setDescription("Reply with unknown content");
-            embed.setAuthor("Unknown");
         }
-        return [embed, reponseText];
+        // For some reason we failed to get the event, so using fallback.
+        const embed = new Discord.RichEmbed();
+        embed.setDescription("Reply with unknown content");
+        embed.setAuthor("Unknown");
+        return embed;
     }
 
     private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: IMatrixEvent | null) {
diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a0a4c550d4d051d936568afb81603e2986385ec3
--- /dev/null
+++ b/src/matrixmessageprocessor.ts
@@ -0,0 +1,313 @@
+import * as Discord from "discord.js";
+import { IMatrixMessage, IMatrixEvent } from "./matrixtypes";
+import * as Parser from "node-html-parser";
+import { Util } from "./util";
+import { DiscordBot } from "./bot";
+import { Client as MatrixClient } from "matrix-js-sdk";
+
+const MIN_NAME_LENGTH = 2;
+const MAX_NAME_LENGTH = 32;
+const MATRIX_TO_LINK = "https://matrix.to/#/";
+
+export interface IMatrixMessageProcessorParams {
+    displayname?: string;
+    mxClient?: MatrixClient;
+    roomId?: string;
+    userId?: string;
+}
+
+export class MatrixMessageProcessor {
+    private guild: Discord.Guild;
+    private listDepth: number = 0;
+    private listBulletPoints: string[] = ["●", "○", "■", "‣"];
+    private params?: IMatrixMessageProcessorParams;
+    constructor(public bot: DiscordBot) { }
+    public async FormatMessage(
+        msg: IMatrixMessage,
+        guild: Discord.Guild,
+        params?: IMatrixMessageProcessorParams,
+    ): Promise<string> {
+        this.guild = guild;
+        this.listDepth = 0;
+        this.params = params;
+        let reply = "";
+        if (msg.formatted_body) {
+            // parser needs everything wrapped in html elements
+            // so we wrap everything in <div> just to be sure stuff is wrapped
+            // as <div> will be un-touched anyways
+            const parsed = Parser.parse(`<div>${msg.formatted_body}</div>`, {
+                lowerCaseTagName: true,
+                pre: true,
+            // tslint:disable-next-line no-any
+            } as any);
+            reply = await this.walkNode(parsed);
+            reply = reply.replace(/\s*$/, ""); // trim off whitespace at end
+        } else {
+            reply = await this.escapeDiscord(msg.body);
+        }
+
+        if (msg.msgtype === "m.emote") {
+            if (params &&
+                params.displayname &&
+                params.displayname.length >= MIN_NAME_LENGTH &&
+                params.displayname.length <= MAX_NAME_LENGTH) {
+                reply = `_${params.displayname} ${reply}_`;
+            } else {
+                reply = `_${reply}_`;
+            }
+        }
+        return reply;
+    }
+
+    private async escapeDiscord(msg: string): Promise<string> {
+        // \u200B is the zero-width space --> they still look the same but don't mention
+        msg = msg.replace(/@everyone/g, "@\u200Beveryone");
+        msg = msg.replace(/@here/g, "@\u200Bhere");
+
+        if (msg.includes("@room") && this.params && this.params.mxClient && this.params.roomId && this.params.userId) {
+            // let's check for more complex logic if @room should be replaced
+            const res: IMatrixEvent = await this.params.mxClient.getStateEvent(
+                this.params.roomId, "m.room.power_levels");
+            if (
+                res && res.users
+                && res.users[this.params.userId] !== undefined
+                && res.notifications
+                && res.notifications.room !== undefined
+                && res.users[this.params.userId] >= res.notifications.room
+            ) {
+                msg = msg.replace(/@room/g, "@here");
+            }
+        }
+        const escapeChars = ["\\", "*", "_", "~", "`"];
+        msg = msg.split(" ").map((s) => {
+            if (s.match(/^https?:\/\//)) {
+                return s;
+            }
+            escapeChars.forEach((char) => {
+                s = s.replace(new RegExp("\\" + char, "g"), "\\" + char);
+            });
+            return s;
+        }).join(" ");
+        return msg;
+    }
+
+    private parsePreContent(node: Parser.HTMLElement): string {
+        let text = node.text;
+        const match = text.match(/^<code([^>]*)>/i);
+        if (!match) {
+            if (text[0] !== "\n") {
+                text = "\n" + text;
+            }
+            return text;
+        }
+        // remove <code> opening-tag
+        text = text.substr(match[0].length);
+        // remove </code> closing tag
+        text = text.replace(/<\/code>$/i, "");
+        if (text[0] !== "\n") {
+            text = "\n" + text;
+        }
+        const language = match[1].match(/language-(\w*)/i);
+        if (language) {
+            text = language[1] + text;
+        }
+        return text;
+    }
+
+    private parseUser(id: string): string {
+        const USER_REGEX = /^@_discord_([0-9]*)/;
+        const match = id.match(USER_REGEX);
+        if (!match || !this.guild.members.get(match[1])) {
+            return "";
+        }
+        return `<@${match[1]}>`;
+    }
+
+    private parseChannel(id: string): string {
+        const CHANNEL_REGEX = /^#_discord_[0-9]*_([0-9]*)/;
+        const match = id.match(CHANNEL_REGEX);
+        if (!match || !this.guild.channels.get(match[1])) {
+            return MATRIX_TO_LINK + id;
+        }
+        return `<#${match[1]}>`;
+    }
+
+    private async parseLinkContent(node: Parser.HTMLElement): Promise<string> {
+        const attrs = node.attributes;
+        const content = await this.walkChildNodes(node);
+        if (!attrs.href || content === attrs.href) {
+            return content;
+        }
+        return `[${content}](${attrs.href})`;
+    }
+
+    private async parsePillContent(node: Parser.HTMLElement): Promise<string> {
+        const attrs = node.attributes;
+        if (!attrs.href || !attrs.href.startsWith(MATRIX_TO_LINK)) {
+            return await this.parseLinkContent(node);
+        }
+        const id = attrs.href.replace(MATRIX_TO_LINK, "");
+        let reply = "";
+        switch (id[0]) {
+            case "@":
+                // user pill
+                reply = this.parseUser(id);
+                break;
+            case "#":
+                reply = this.parseChannel(id);
+                break;
+        }
+        if (!reply) {
+            return await this.parseLinkContent(node);
+        }
+        return reply;
+    }
+
+    private async parseImageContent(node: Parser.HTMLElement): Promise<string> {
+        const EMOTE_NAME_REGEX = /^:?(\w+):?/;
+        const attrs = node.attributes;
+        const name = attrs.alt || attrs.title || "";
+        let emoji: Discord.Emoji | null = null;
+        // first check for matching mxc url
+        if (attrs.src) {
+            let id = "";
+            try {
+                const emojiDb = await this.bot.GetEmojiByMxc(attrs.src);
+                id = emojiDb.EmojiId;
+                emoji = this.guild.emojis.find((e) => e.id === id);
+            } catch (e) {
+                emoji = null;
+            }
+        }
+        // nexc check for matching alt text / title
+        if (!emoji) {
+            const match = name.match(EMOTE_NAME_REGEX);
+            let emojiName = "";
+            if (match) {
+                emojiName = match[1];
+                emoji = this.guild.emojis.find((e) => e.name === emojiName);
+            }
+        }
+
+        if (!emoji) {
+            return await this.escapeDiscord(name);
+        }
+        return `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`;
+    }
+
+    private async parseBlockquoteContent(node: Parser.HTMLElement): Promise<string> {
+        let msg = await this.walkChildNodes(node);
+
+        msg = msg.split("\n").map((s) => {
+            return "> " + s;
+        }).join("\n");
+        msg = msg + "\n\n";
+        return msg;
+    }
+
+    private async parseUlContent(node: Parser.HTMLElement): Promise<string> {
+        this.listDepth++;
+        const entries = await this.arrayChildNodes(node, ["li"]);
+        this.listDepth--;
+        const bulletPoint = this.listBulletPoints[this.listDepth % this.listBulletPoints.length];
+
+        let msg = entries.map((s) => {
+            return `${"    ".repeat(this.listDepth)}${bulletPoint} ${s}`;
+        }).join("\n");
+
+        if (this.listDepth === 0) {
+            msg = `\n${msg}\n\n`;
+        }
+        return msg;
+    }
+
+    private async parseOlContent(node: Parser.HTMLElement): Promise<string> {
+        this.listDepth++;
+        const entries = await this.arrayChildNodes(node, ["li"]);
+        this.listDepth--;
+        let entry = 0;
+        const attrs = node.attributes;
+        if (attrs.start && attrs.start.match(/^[0-9]+$/)) {
+            entry = parseInt(attrs.start, 10) - 1;
+        }
+
+        let msg = entries.map((s) => {
+            entry++;
+            return `${"    ".repeat(this.listDepth)}${entry}. ${s}`;
+        }).join("\n");
+
+        if (this.listDepth === 0) {
+            msg = `\n${msg}\n\n`;
+        }
+        return msg;
+    }
+
+    private async arrayChildNodes(node: Parser.Node, types: string[] = []): Promise<string[]> {
+        const replies: string[] = [];
+        await Util.AsyncForEach(node.childNodes, async (child) => {
+            if (types.length && (
+                child.nodeType === Parser.NodeType.TEXT_NODE
+                || !types.includes((child as Parser.HTMLElement).tagName)
+            )) {
+                return;
+            }
+            replies.push(await this.walkNode(child));
+        });
+        return replies;
+    }
+
+    private async walkChildNodes(node: Parser.Node): Promise<string> {
+        let reply = "";
+        await Util.AsyncForEach(node.childNodes, async (child) => {
+            reply += await this.walkNode(child);
+        });
+        return reply;
+    }
+
+    private async walkNode(node: Parser.Node): Promise<string> {
+        if (node.nodeType === Parser.NodeType.TEXT_NODE) {
+            // ignore \n between single nodes
+            if ((node as Parser.TextNode).text === "\n") {
+                return "";
+            }
+            return await this.escapeDiscord((node as Parser.TextNode).text);
+        } else if (node.nodeType === Parser.NodeType.ELEMENT_NODE) {
+            const nodeHtml = node as Parser.HTMLElement;
+            switch (nodeHtml.tagName) {
+                case "em":
+                case "i":
+                    return `*${await this.walkChildNodes(nodeHtml)}*`;
+                case "strong":
+                case "b":
+                    return `**${await this.walkChildNodes(nodeHtml)}**`;
+                case "u":
+                    return `__${await this.walkChildNodes(nodeHtml)}__`;
+                case "del":
+                    return `~~${await this.walkChildNodes(nodeHtml)}~~`;
+                case "code":
+                    return `\`${nodeHtml.text}\``;
+                case "pre":
+                    return `\`\`\`${this.parsePreContent(nodeHtml)}\`\`\``;
+                case "a":
+                    return await this.parsePillContent(nodeHtml);
+                case "img":
+                    return await this.parseImageContent(nodeHtml);
+                case "br":
+                    return "\n";
+                case "blockquote":
+                    return await this.parseBlockquoteContent(nodeHtml);
+                case "ul":
+                    return await this.parseUlContent(nodeHtml);
+                case "ol":
+                    return await this.parseOlContent(nodeHtml);
+                case "mx-reply":
+                    return "";
+                case "hr":
+                    return "\n----------\n";
+                default:
+                    return await this.walkChildNodes(nodeHtml);
+            }
+        }
+        return "";
+    }
+}
diff --git a/src/matrixtypes.ts b/src/matrixtypes.ts
index 4efbf8395084b61ef852d59886b6eb5e9cb5058e..9e72460f93ae884d79ca4f4e37fd196813939661 100644
--- a/src/matrixtypes.ts
+++ b/src/matrixtypes.ts
@@ -23,4 +23,13 @@ export interface IMatrixEvent {
     content?: IMatrixEventContent;
     unsigned?: any; // tslint:disable-line no-any
     origin_server_ts?: number;
+    users?: any; // tslint:disable-line no-any
+    notifications?: any; // tslint:disable-line no-any
+}
+
+export interface IMatrixMessage {
+    body: string;
+    msgtype: string;
+    formatted_body?: string;
+    format?: string;
 }
diff --git a/src/util.ts b/src/util.ts
index 8661ba785ba9f14f60a923e97649ba00b0a638dc..a83be839c2a1c25a529a1c90c0360be4067b2083 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -238,17 +238,6 @@ export class Util {
         return {command, args};
     }
 
-    public static GetReplyFromReplyBody(body: string) {
-        const lines = body.split("\n");
-        while (lines[0].startsWith("> ") || lines[0].trim().length === 0) {
-            lines.splice(0, 1);
-            if (lines.length === 0) {
-                return "";
-            }
-        }
-        return lines.join("\n").trim();
-    }
-
     public static async AsyncForEach(arr, callback) {
         for (let i = 0; i < arr.length; i++) {
             await callback(arr[i], i, arr);
diff --git a/test/mocks/emoji.ts b/test/mocks/emoji.ts
index c499ca4f03b19da04d787401691dc50e9db5e05d..b3809625d44f6cda58fbdbeecc97b99541c87b18 100644
--- a/test/mocks/emoji.ts
+++ b/test/mocks/emoji.ts
@@ -3,5 +3,5 @@
 /* tslint:disable:no-unused-expression max-file-line-count no-any */
 
 export class MockEmoji {
-    constructor(public id: string = "", public name = "") { }
+    constructor(public id: string = "", public name = "", public animated = false) { }
 }
diff --git a/test/test_channelsyncroniser.ts b/test/test_channelsyncroniser.ts
index 36bbc94812c80dbedf0b9864187bbb9147e33cdd..91e626365efae3dae121291be55bac7b45a93fea 100644
--- a/test/test_channelsyncroniser.ts
+++ b/test/test_channelsyncroniser.ts
@@ -8,7 +8,6 @@ import { MockGuild } from "./mocks/guild";
 import { MockMember } from "./mocks/member";
 import { MatrixEventProcessor, MatrixEventProcessorOpts } from "../src/matrixeventprocessor";
 import { DiscordBridgeConfig } from "../src/config";
-import { MessageProcessor, MessageProcessorOpts } from "../src/messageprocessor";
 import { MockChannel } from "./mocks/channel";
 import { Bridge, MatrixRoom, RemoteRoom } from "matrix-appservice-bridge";
 
diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts
index bf387306a26df4ba7dda278e7c85ecc920b31e8d..2e7bb00dadfcd3742e5cc05f31351dcf243da14f 100644
--- a/test/test_discordbot.ts
+++ b/test/test_discordbot.ts
@@ -3,7 +3,6 @@ import * as Proxyquire from "proxyquire";
 import * as Discord from "discord.js";
 import { Log } from "../src/log";
 
-import { MessageProcessorMatrixResult } from "../src/messageprocessor";
 import { MockGuild } from "./mocks/guild";
 import { MockMember } from "./mocks/member";
 import { DiscordBot } from "../src/bot";
diff --git a/test/test_messageprocessor.ts b/test/test_discordmessageprocessor.ts
similarity index 80%
rename from test/test_messageprocessor.ts
rename to test/test_discordmessageprocessor.ts
index 0ec88eb300a2e36a0cf35e4575ff1de02c594075..95e098986053bdc6ea1b413e80ebc322a456e9a9 100644
--- a/test/test_messageprocessor.ts
+++ b/test/test_discordmessageprocessor.ts
@@ -1,6 +1,6 @@
 import * as Chai from "chai";
 import * as Discord from "discord.js";
-import { MessageProcessor, MessageProcessorOpts } from "../src/messageprocessor";
+import { DiscordMessageProcessor, DiscordMessageProcessorOpts } from "../src/discordmessageprocessor";
 import { DiscordBot } from "../src/bot";
 import { MockGuild } from "./mocks/guild";
 import { MockMember } from "./mocks/member";
@@ -20,53 +20,57 @@ const bot = {
     },
 };
 
-describe("MessageProcessor", () => {
+describe("DiscordMessageProcessor", () => {
     describe("init", () => {
         it("constructor", () => {
-            const mp = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const mp = new DiscordMessageProcessor(new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
         });
     });
-    describe("FormatDiscordMessage", () => {
+    describe("FormatMessage", () => {
         it("processes plain text messages correctly", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [];
             msg.content = "Hello World!";
-            const result = await processor.FormatDiscordMessage(msg);
+            const result = await processor.FormatMessage(msg);
             Chai.assert(result.body, "Hello World!");
             Chai.assert(result.formattedBody, "Hello World!");
         });
         it("processes markdown messages correctly.", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [];
             msg.content = "Hello *World*!";
-            const result = await processor.FormatDiscordMessage(msg);
+            const result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.body, "Hello *World*!");
             Chai.assert.equal(result.formattedBody, "Hello <em>World</em>!");
         });
         it("processes non-discord markdown correctly.", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [];
             msg.content = "> inb4 tests";
-            let result = await processor.FormatDiscordMessage(msg);
+            let result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.body, "> inb4 tests");
             Chai.assert.equal(result.formattedBody, "&gt; inb4 tests");
 
             msg.embeds = [];
             msg.content = "[test](http://example.com)";
-            result = await processor.FormatDiscordMessage(msg);
+            result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.body, "[test](http://example.com)");
             Chai.assert.equal(result.formattedBody,
                 "[test](<a href=\"http://example.com\">http://example.com</a>)");
         });
         it("processes discord-specific markdown correctly.", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [];
             msg.content = "_ italic _";
-            const result = await processor.FormatDiscordMessage(msg);
+            const result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.body, "_ italic _");
             Chai.assert.equal(result.formattedBody, "<em> italic </em>");
         });
@@ -101,7 +105,8 @@ describe("MessageProcessor", () => {
     });
     describe("FormatEmbeds", () => {
         it("should format embeds correctly", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 {
@@ -125,13 +130,14 @@ describe("MessageProcessor", () => {
                 },
             ];
             msg.content = "message";
-            const result = await processor.FormatDiscordMessage(msg);
+            const result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.body, "message\n\n----\n##### [Title](http://example.com)\nDescription");
             Chai.assert.equal(result.formattedBody, "message<hr><h5><a href=\"http://example.com\">Title</a>" +
                 "</h5>Description");
         });
         it("should ignore same-url embeds", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 {
@@ -155,7 +161,7 @@ describe("MessageProcessor", () => {
                 },
             ];
             msg.content = "message http://example.com";
-            const result = await processor.FormatDiscordMessage(msg);
+            const result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.body, "message http://example.com");
             Chai.assert.equal(result.formattedBody, "message <a href=\"http://example.com\">" +
                 "http://example.com</a>");
@@ -163,7 +169,8 @@ describe("MessageProcessor", () => {
     });
     describe("FormatEdit", () => {
         it("should format basic edits appropriately", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const oldMsg = new MockMessage() as any;
             const newMsg = new MockMessage() as any;
             oldMsg.embeds = [];
@@ -179,7 +186,8 @@ describe("MessageProcessor", () => {
         });
 
         it("should format markdown heavy edits apropriately", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const oldMsg = new MockMessage() as any;
             const newMsg = new MockMessage() as any;
             oldMsg.embeds = [];
@@ -199,7 +207,8 @@ describe("MessageProcessor", () => {
 
     describe("InsertUser / HTML", () => {
         it("processes members missing from the guild correctly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             const channel = new Discord.TextChannel(guild, {});
             const msg = new MockMessage(channel) as any;
@@ -212,7 +221,8 @@ describe("MessageProcessor", () => {
                 "<a href=\"https://matrix.to/#/@_discord_12345:localhost\">@_discord_12345:localhost</a>");
         });
         it("processes members with usernames correctly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             guild._mockAddMember(new MockMember("12345", "TestUsername"));
             const channel = new Discord.TextChannel(guild, {});
@@ -226,7 +236,8 @@ describe("MessageProcessor", () => {
                 "<a href=\"https://matrix.to/#/@_discord_12345:localhost\">TestUsername</a>");
         });
         it("processes members with nickname correctly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             guild._mockAddMember(new MockMember("12345", "TestUsername", null, "TestNickname"));
             const channel = new Discord.TextChannel(guild, {});
@@ -242,7 +253,8 @@ describe("MessageProcessor", () => {
     });
     describe("InsertChannel / HTML", () => {
         it("processes unknown channel correctly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
             const msg = new MockMessage(channel) as any;
@@ -255,7 +267,8 @@ describe("MessageProcessor", () => {
                 "<a href=\"https://matrix.to/#/#_discord_123_123456789:localhost\">#123456789</a>");
         });
         it("processes channels correctly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
             guild.channels.set("456", channel);
@@ -271,7 +284,8 @@ describe("MessageProcessor", () => {
     });
     describe("InsertRole / HTML", () => {
         it("ignores unknown roles", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
             guild.channels.set("456", channel);
@@ -286,7 +300,8 @@ describe("MessageProcessor", () => {
             Chai.assert.equal(reply, "&lt;@&amp;1234&gt;");
         });
         it("parses known roles", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
             guild.channels.set("456", channel);
@@ -304,7 +319,8 @@ describe("MessageProcessor", () => {
     });
     describe("InsertEmoji", () => {
         it("inserts static emojis to their post-parse flag", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const content = {
                 animated: false,
                 id: "1234",
@@ -314,7 +330,8 @@ describe("MessageProcessor", () => {
             Chai.assert.equal(reply, "\x01blah\x010\x011234\x01");
         });
         it("inserts animated emojis to their post-parse flag", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const content = {
                 animated: true,
                 id: "1234",
@@ -326,7 +343,8 @@ describe("MessageProcessor", () => {
     });
     describe("InsertMxcImages / HTML", () => {
         it("processes unknown emoji correctly", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
             const msg = new MockMessage(channel) as any;
@@ -338,7 +356,8 @@ describe("MessageProcessor", () => {
             Chai.assert.equal(reply, "Hello &lt;:hello:123456789&gt;");
         });
         it("processes emoji correctly", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const guild: any = new MockGuild("123", []);
             const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"});
             guild.channels.set("456", channel);
@@ -353,7 +372,8 @@ describe("MessageProcessor", () => {
     });
     describe("InsertEmbeds", () => {
         it("processes titleless embeds properly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 new Discord.MessageEmbed(msg, {
@@ -365,7 +385,8 @@ describe("MessageProcessor", () => {
             Chai.assert.equal(content, "\n\n----\nTestDescription");
         });
         it("processes urlless embeds properly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 new Discord.MessageEmbed(msg, {
@@ -378,7 +399,8 @@ describe("MessageProcessor", () => {
             Chai.assert.equal(content, "\n\n----\n##### TestTitle\nTestDescription");
         });
         it("processes linked embeds properly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 new Discord.MessageEmbed(msg, {
@@ -392,7 +414,8 @@ describe("MessageProcessor", () => {
             Chai.assert.equal(content, "\n\n----\n##### [TestTitle](testurl)\nTestDescription");
         });
         it("rejects titleless and descriptionless embeds", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 new Discord.MessageEmbed(msg, {
@@ -404,7 +427,8 @@ describe("MessageProcessor", () => {
             Chai.assert.equal(content, "Some content...");
         });
         it("processes multiple embeds properly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 new Discord.MessageEmbed(msg, {
@@ -426,7 +450,8 @@ describe("MessageProcessor", () => {
             );
         });
         it("inserts embeds properly", () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [
                 new Discord.MessageEmbed(msg, {
@@ -449,21 +474,23 @@ TestDescription`,
     });
     describe("Message Type", () => {
         it("sets non-bot messages as m.text", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [];
             msg.content = "no bot";
             msg.author.bot = false;
-            const result = await processor.FormatDiscordMessage(msg);
+            const result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.msgtype, "m.text");
         });
         it("sets bot messages as m.notice", async () => {
-            const processor = new MessageProcessor(new MessageProcessorOpts("localhost"), bot as DiscordBot);
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
             const msg = new MockMessage() as any;
             msg.embeds = [];
             msg.content = "a bot";
             msg.author.bot = true;
-            const result = await processor.FormatDiscordMessage(msg);
+            const result = await processor.FormatMessage(msg);
             Chai.assert.equal(result.msgtype, "m.notice");
         });
     });
diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts
index 9073fc8b028f0730f2936d0a7d10564b8c41a47e..3d61f0ee22444532ac03cb2aaa214b07d313a770 100644
--- a/test/test_matrixeventprocessor.ts
+++ b/test/test_matrixeventprocessor.ts
@@ -10,7 +10,6 @@ import { MockMember } from "./mocks/member";
 import { MockEmoji } from "./mocks/emoji";
 import { MatrixEventProcessor, MatrixEventProcessorOpts } from "../src/matrixeventprocessor";
 import { DiscordBridgeConfig } from "../src/config";
-import { MessageProcessor, MessageProcessorOpts } from "../src/messageprocessor";
 import { MockChannel } from "./mocks/channel";
 import { IMatrixEvent } from "../src/matrixtypes";
 
@@ -32,16 +31,35 @@ const bot = {
 };
 
 const mxClient = {
+    getStateEvent: async (roomId, stateType, stateKey) => {
+        if (stateType === "m.room.member") {
+            switch (stateKey) {
+                case "@test:localhost":
+                    return {
+                        avatar_url: "mxc://localhost/avatarurl",
+                        displayname: "Test User",
+                    };
+                case "@test_short:localhost":
+                    return {
+                        avatar_url: "mxc://localhost/avatarurl",
+                        displayname: "t",
+                    };
+                case "@test_long:localhost":
+                    return {
+                        avatar_url: "mxc://localhost/avatarurl",
+                        displayname: "this is a very very long displayname that should be capped",
+                    };
+            }
+            return null;
+        }
+        return { };
+    },
     mxcUrlToHttp: (url) => {
         return url.replace("mxc://", "https://");
     },
 };
 
-function createMatrixEventProcessor(
-    disableMentions: boolean = false,
-    disableEveryone = false,
-    disableHere = false,
-): MatrixEventProcessor {
+function createMatrixEventProcessor(): MatrixEventProcessor {
     const bridge = {
         getBot: () => {
             return {
@@ -78,6 +96,9 @@ function createMatrixEventProcessor(
                                 "body": `> <@doggo:localhost> This is the original body
 
                                 This is the first reply`,
+                                "formatted_body": `
+<mx-reply><blockquote><a>In Reply to</a> <a>@doggo:localhost</a>
+<br>This is the original body</blockquote></mx-reply>This is the first reply`,
                                 "m.relates_to": {
                                     "m.in_reply_to": {
                                         event_id: "$goodEvent:localhost",
@@ -109,9 +130,6 @@ function createMatrixEventProcessor(
         },
     };
     const config = new DiscordBridgeConfig();
-    config.bridge.disableDiscordMentions = disableMentions;
-    config.bridge.disableEveryoneMention = disableEveryone;
-    config.bridge.disableHereMention = disableHere;
 
     const Util = Object.assign(require("../src/util").Util, {
         DownloadFile: (name: string) => {
@@ -265,10 +283,6 @@ describe("MatrixEventProcessor", () => {
                     body: "testcontent",
                 },
                 sender: "@test:localhost",
-            } as IMatrixEvent,
-            {
-                avatar_url: "mxc://localhost/avatarurl",
-                displayname: "Test User",
             } as IMatrixEvent, mockChannel as any);
             const author = embeds.messageEmbed.author;
             Chai.assert.equal(author!.name, "Test User");
@@ -283,12 +297,10 @@ describe("MatrixEventProcessor", () => {
                     body: "testcontent",
                 },
                 sender: "@test:localhost",
-            } as IMatrixEvent, {
-                displayname: "Test User",
             } as IMatrixEvent, mockChannel as any);
             const author = embeds.messageEmbed.author;
             Chai.assert.equal(author!.name, "Test User");
-            Chai.assert.isUndefined(author!.icon_url);
+            Chai.assert.equal(author!.icon_url, "https://localhost/avatarurl");
             Chai.assert.equal(author!.url, "https://matrix.to/#/@test:localhost");
         });
 
@@ -298,12 +310,12 @@ describe("MatrixEventProcessor", () => {
                 content: {
                     body: "testcontent",
                 },
-                sender: "@test:localhost",
-            } as IMatrixEvent, null, mockChannel as any);
+                sender: "@test_nonexistant:localhost",
+            } as IMatrixEvent, mockChannel as any);
             const author = embeds.messageEmbed.author;
-            Chai.assert.equal(author!.name, "@test:localhost");
+            Chai.assert.equal(author!.name, "@test_nonexistant:localhost");
             Chai.assert.isUndefined(author!.icon_url);
-            Chai.assert.equal(author!.url, "https://matrix.to/#/@test:localhost");
+            Chai.assert.equal(author!.url, "https://matrix.to/#/@test_nonexistant:localhost");
         });
 
         it("Should use the userid when the displayname is too short", async () => {
@@ -312,12 +324,10 @@ describe("MatrixEventProcessor", () => {
                 content: {
                     body: "testcontent",
                 },
-                sender: "@test:localhost",
-            } as IMatrixEvent, {
-                displayname: "t",
+                sender: "@test_short:localhost",
             } as IMatrixEvent, mockChannel as any);
             const author = embeds.messageEmbed.author;
-            Chai.assert.equal(author!.name, "@test:localhost");
+            Chai.assert.equal(author!.name, "@test_short:localhost");
         });
 
         it("Should use the userid when displayname is too long", async () => {
@@ -326,12 +336,10 @@ describe("MatrixEventProcessor", () => {
                 content: {
                     body: "testcontent",
                 },
-                sender: "@test:localhost",
-            } as IMatrixEvent, {
-                displayname: "this is a very very long displayname that should be capped",
+                sender: "@test_long:localhost",
             } as IMatrixEvent, mockChannel as any);
             const author = embeds.messageEmbed.author;
-            Chai.assert.equal(author!.name, "@test:localhost");
+            Chai.assert.equal(author!.name, "@test_long:localhost");
         });
 
         it("Should cap the sender name if it is too long", async () => {
@@ -341,7 +349,7 @@ describe("MatrixEventProcessor", () => {
                     body: "testcontent",
                 },
                 sender: "@testwithalottosayaboutitselfthatwillgoonandonandonandon:localhost",
-            } as IMatrixEvent, null, mockChannel as any);
+            } as IMatrixEvent, mockChannel as any);
             const author = embeds.messageEmbed.author;
             Chai.assert.equal(author!.name, "@testwithalottosayaboutitselftha");
         });
@@ -353,112 +361,35 @@ describe("MatrixEventProcessor", () => {
                     body: "testcontent",
                 },
                 sender: "@test:localhost",
-            } as IMatrixEvent, {
-                avatar_url: "mxc://localhost/test",
             } as IMatrixEvent, mockChannel as any);
             const author = embeds.messageEmbed.author;
-            Chai.assert.equal(author!.name, "@test:localhost");
-            Chai.assert.equal(author!.icon_url, "https://localhost/test");
+            Chai.assert.equal(author!.name, "Test User");
+            Chai.assert.equal(author!.icon_url, "https://localhost/avatarurl");
             Chai.assert.equal(author!.url, "https://matrix.to/#/@test:localhost");
         });
 
-        it("Should enable mentions if configured.", async () => {
+        it("Should remove everyone mentions.", async () => {
             const processor = createMatrixEventProcessor();
-            const embeds = await processor.EventToEmbed({
-                content: {
-                    body: "@testuser2 Hello!",
-                },
-                sender: "@test:localhost",
-            } as IMatrixEvent, {
-                avatar_url: "test",
-            } as IMatrixEvent, mockChannel as any);
-            Chai.assert.equal(embeds.messageEmbed.description, "<@!12345> Hello!");
-        });
-
-        it("Should disable mentions if configured.", async () => {
-            const processor = createMatrixEventProcessor(true);
-            const embeds = await processor.EventToEmbed({
-                content: {
-                    body: "@testuser2 Hello!",
-                },
-                sender: "@test:localhost",
-            } as IMatrixEvent, {
-                avatar_url: "test",
-            } as IMatrixEvent, mockChannel as any);
-            Chai.assert.equal(embeds.messageEmbed.description, "@testuser2 Hello!");
-        });
-
-        it("Should remove everyone mentions if configured.", async () => {
-            const processor = createMatrixEventProcessor(false, true);
             const embeds = await processor.EventToEmbed({
                 content: {
                     body: "@everyone Hello!",
                 },
                 sender: "@test:localhost",
-            } as IMatrixEvent, {
-                avatar_url: "test",
             } as IMatrixEvent, mockChannel as any);
-            Chai.assert.equal(embeds.messageEmbed.description, "@ everyone Hello!");
+            Chai.assert.equal(embeds.messageEmbed.description, "@\u200Beveryone Hello!");
         });
 
-        it("Should remove here mentions if configured.", async () => {
-            const processor = createMatrixEventProcessor(false, false, true);
+        it("Should remove here mentions.", async () => {
+            const processor = createMatrixEventProcessor();
             const embeds = await processor.EventToEmbed({
                 content: {
                     body: "@here Hello!",
                 },
                 sender: "@test:localhost",
-            } as IMatrixEvent, {
-                avatar_url: "test",
             } as IMatrixEvent, mockChannel as any);
-            Chai.assert.equal(embeds.messageEmbed.description, "@ here Hello!");
+            Chai.assert.equal(embeds.messageEmbed.description, "@\u200Bhere Hello!");
         });
 
-        it("Should process custom discord emojis.", async () => {
-            const processor = createMatrixEventProcessor(false, false, true);
-            const mockEmoji = new MockEmoji("123", "supercake");
-            const mockCollectionEmojis = new MockCollection<string, MockEmoji>();
-            mockCollectionEmojis.set("123", mockEmoji);
-
-            const mockChannelEmojis = new MockChannel("test", {
-                emojis: mockCollectionEmojis,
-            });
-            const embeds = await processor.EventToEmbed({
-                content: {
-                    body: "I like :supercake:",
-                },
-                sender: "@test:localhost",
-            } as IMatrixEvent, {
-                avatar_url: "test",
-            } as IMatrixEvent, mockChannelEmojis as any);
-            Chai.assert.equal(
-                embeds.messageEmbed.description,
-                "I like <:supercake:123>",
-            );
-        });
-
-        it("Should not process invalid custom discord emojis.", async () => {
-            const processor = createMatrixEventProcessor(false, false, true);
-            const mockEmoji = new MockEmoji("123", "supercake");
-            const mockCollectionEmojis = new MockCollection<string, MockEmoji>();
-            mockCollectionEmojis.set("123", mockEmoji);
-
-            const mockChannelEmojis = new MockChannel("test", {
-                emojis: mockCollectionEmojis,
-            });
-            const embeds = await processor.EventToEmbed({
-                content: {
-                    body: "I like :lamecake:",
-                },
-                sender: "@test:localhost",
-            } as IMatrixEvent, {
-                avatar_url: "test",
-            } as IMatrixEvent, mockChannelEmojis as any);
-            Chai.assert.equal(
-                embeds.messageEmbed.description,
-                "I like :lamecake:",
-            );
-        });
         it("Should replace /me with * displayname, and italicize message", async () => {
             const processor = createMatrixEventProcessor();
             const embeds = await processor.EventToEmbed({
@@ -467,12 +398,10 @@ describe("MatrixEventProcessor", () => {
                     msgtype: "m.emote",
                 },
                 sender: "@test:localhost",
-            } as IMatrixEvent, {
-                displayname: "displayname",
             } as IMatrixEvent, mockChannel as any);
             Chai.assert.equal(
                 embeds.messageEmbed.description,
-                "*displayname likes puppies*",
+                "_Test User likes puppies_",
             );
         });
         it("Should handle stickers.", async () => {
@@ -484,118 +413,10 @@ describe("MatrixEventProcessor", () => {
                 },
                 sender: "@test:localhost",
                 type: "m.sticker",
-            } as IMatrixEvent, {
-                avatar_url: "test",
             } as IMatrixEvent, mockChannel as any);
             Chai.assert.equal(embeds.messageEmbed.description, "");
         });
     });
-    describe("FindMentionsInPlainBody", () => {
-        it("processes mentioned username correctly", async () => {
-            const processor = createMatrixEventProcessor();
-            const guild: any = new MockGuild("123", []);
-            const members: Discord.GuildMember[] = [new Discord.GuildMember(guild, {
-                user: {
-                    discriminator: "54321",
-                    id: "12345",
-                    username: "TestUsername",
-                },
-            })];
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("Hello TestUsername", members),
-                "Hello <@!12345>",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("Hello TestUsername#54321", members),
-                "Hello <@!12345>",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("I really love going to https://TestUsername.com", members),
-                "I really love going to https://TestUsername.com",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("I really love going to www.TestUsername.com", members),
-                "I really love going to www.TestUsername.com",
-            );
-        });
-        it("processes mentioned nickname correctly", async () => {
-            const processor = createMatrixEventProcessor();
-            const guild: any = new MockGuild("123", []);
-            const members: Discord.GuildMember[] = [new Discord.GuildMember(guild, {
-                nick: "Test",
-                user: {
-                    id: "54321",
-                    username: "Test",
-                },
-            }), new Discord.GuildMember(guild, {
-                nick: "TestNickname",
-                user: {
-                    id: "12345",
-                    username: "TestUsername",
-                },
-            }), new Discord.GuildMember(guild, {
-                nick: "𝖘𝖔𝖒𝖊𝖋𝖆𝖓𝖈𝖞𝖓𝖎𝖈𝖐𝖓𝖆𝖒𝖊",
-                user: {
-                    id: "66666",
-                    username: "SomeFancyNickname",
-                },
-            })];
-            Chai.assert.equal(processor.FindMentionsInPlainBody("Hello TestNickname", members), "Hello <@!12345>");
-            Chai.assert.equal(processor.FindMentionsInPlainBody("TestNickname: Hello", members), "<@!12345>: Hello");
-            Chai.assert.equal(processor.FindMentionsInPlainBody("TestNickname, Hello", members), "<@!12345>, Hello");
-            Chai.assert.equal(processor.FindMentionsInPlainBody("TestNickname Hello", members), "<@!12345> Hello");
-            Chai.assert.equal(processor.FindMentionsInPlainBody("testNicKName Hello", members), "<@!12345> Hello");
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("𝖘𝖔𝖒𝖊𝖋𝖆𝖓𝖈𝖞𝖓𝖎𝖈𝖐𝖓𝖆𝖒𝖊 Hello", members),
-                "<@!66666> Hello",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("I wish TestNickname was here", members),
-                "I wish <@!12345> was here",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("I wish TestNickname was here, TestNickname is cool", members),
-                "I wish <@!12345> was here, <@!12345> is cool",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("TestNickname was here with Test", members),
-                "<@!12345> was here with <@!54321>",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("Fixing this issue provided by @Test", members),
-                "Fixing this issue provided by <@!54321>",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("I really love going to https://Test.com", members),
-                "I really love going to https://Test.com",
-            );
-            Chai.assert.equal(
-                processor.FindMentionsInPlainBody("I really love going to www.Test.com", members),
-                "I really love going to www.Test.com",
-            );
-        });
-        it("processes non-mentions correctly", async () => {
-            const processor = createMatrixEventProcessor();
-            const guild: any = new MockGuild("123", []);
-            const members: Discord.GuildMember[] = [new Discord.GuildMember(guild, {
-                nick: "that",
-                user: {
-                    id: "12345",
-                    username: "TestUsername",
-                },
-            }),
-                new Discord.GuildMember(guild, {
-                    nick: "testingstring",
-                    user: {
-                        id: "12345",
-                        username: "that",
-                    },
-                })];
-            const msg = "Welcome thatman";
-            const content = processor.FindMentionsInPlainBody(msg, members);
-            Chai.assert.equal(content, "Welcome thatman");
-        });
-    });
     describe("HandleAttachment", () => {
         const SMALL_FILE = 200;
         it("message without an attachment", async () => {
@@ -700,7 +521,7 @@ describe("MatrixEventProcessor", () => {
                 },
                 sender: "@test:localhost",
                 type: "m.room.message",
-            } as IMatrixEvent);
+            } as IMatrixEvent, mockChannel as any);
             expect(result).to.be.undefined;
         });
         it("should handle replies without a fallback", async () => {
@@ -716,12 +537,11 @@ describe("MatrixEventProcessor", () => {
                 },
                 sender: "@test:localhost",
                 type: "m.room.message",
-            } as IMatrixEvent);
-            expect(result![0].description).to.be.equal("Hello!");
-            expect(result![0].author!.name).to.be.equal("Doggo!");
-            expect(result![0].author!.icon_url).to.be.equal("https://fakeurl.com");
-            expect(result![0].author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
-            expect(result![1]).to.be.equal("Test");
+            } as IMatrixEvent, mockChannel as any);
+            expect(result!.description).to.be.equal("Hello!");
+            expect(result!.author!.name).to.be.equal("Doggo!");
+            expect(result!.author!.icon_url).to.be.equal("https://fakeurl.com");
+            expect(result!.author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
         });
         it("should handle replies with a missing event", async () => {
             const processor = createMatrixEventProcessor();
@@ -738,12 +558,11 @@ This is where the reply goes`,
                 },
                 sender: "@test:localhost",
                 type: "m.room.message",
-            } as IMatrixEvent);
-            expect(result![0].description).to.be.equal("Reply with unknown content");
-            expect(result![0].author!.name).to.be.equal("Unknown");
-            expect(result![0].author!.icon_url).to.be.undefined;
-            expect(result![0].author!.url).to.be.undefined;
-            expect(result![1]).to.be.equal("This is where the reply goes");
+            } as IMatrixEvent, mockChannel as any);
+            expect(result!.description).to.be.equal("Reply with unknown content");
+            expect(result!.author!.name).to.be.equal("Unknown");
+            expect(result!.author!.icon_url).to.be.undefined;
+            expect(result!.author!.url).to.be.undefined;
         });
         it("should handle replies with a valid reply event", async () => {
             const processor = createMatrixEventProcessor();
@@ -760,12 +579,11 @@ This is where the reply goes`,
                 },
                 sender: "@test:localhost",
                 type: "m.room.message",
-            } as IMatrixEvent);
-            expect(result![0].description).to.be.equal("Hello!");
-            expect(result![0].author!.name).to.be.equal("Doggo!");
-            expect(result![0].author!.icon_url).to.be.equal("https://fakeurl.com");
-            expect(result![0].author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
-            expect(result![1]).to.be.equal("This is where the reply goes");
+            } as IMatrixEvent, mockChannel as any);
+            expect(result!.description).to.be.equal("Hello!");
+            expect(result!.author!.name).to.be.equal("Doggo!");
+            expect(result!.author!.icon_url).to.be.equal("https://fakeurl.com");
+            expect(result!.author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
         });
         it("should handle replies on top of replies", async () => {
             const processor = createMatrixEventProcessor();
@@ -782,12 +600,11 @@ This is the second reply`,
                 },
                 sender: "@test:localhost",
                 type: "m.room.message",
-            } as IMatrixEvent);
-            expect(result![0].description).to.be.equal("This is the first reply");
-            expect(result![0].author!.name).to.be.equal("Doggo!");
-            expect(result![0].author!.icon_url).to.be.equal("https://fakeurl.com");
-            expect(result![0].author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
-            expect(result![1]).to.be.equal("This is the second reply");
+            } as IMatrixEvent, mockChannel as any);
+            expect(result!.description).to.be.equal("This is the first reply");
+            expect(result!.author!.name).to.be.equal("Doggo!");
+            expect(result!.author!.icon_url).to.be.equal("https://fakeurl.com");
+            expect(result!.author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
         });
         it("should handle replies with non text events", async () => {
             const processor = createMatrixEventProcessor();
@@ -804,12 +621,11 @@ This is the reply`,
                 },
                 sender: "@test:localhost",
                 type: "m.room.message",
-            } as IMatrixEvent);
-            expect(result![0].description).to.be.equal("Reply with unknown content");
-            expect(result![0].author!.name).to.be.equal("Doggo!");
-            expect(result![0].author!.icon_url).to.be.equal("https://fakeurl.com");
-            expect(result![0].author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
-            expect(result![1]).to.be.equal("This is the reply");
+            } as IMatrixEvent, mockChannel as any);
+            expect(result!.description).to.be.equal("Reply with unknown content");
+            expect(result!.author!.name).to.be.equal("Doggo!");
+            expect(result!.author!.icon_url).to.be.equal("https://fakeurl.com");
+            expect(result!.author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost");
         });
     });
 });
diff --git a/test/test_matrixmessageprocessor.ts b/test/test_matrixmessageprocessor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..611496fd6a9fe833e0aefb17354fff207b4be1df
--- /dev/null
+++ b/test/test_matrixmessageprocessor.ts
@@ -0,0 +1,570 @@
+import * as Chai from "chai";
+import * as Discord from "discord.js";
+import { MockGuild } from "./mocks/guild";
+import { MockMember } from "./mocks/member";
+import { MockChannel } from "./mocks/channel";
+import { MockEmoji } from "./mocks/emoji";
+import { DiscordBot } from "../src/bot";
+import { DbEmoji } from "../src/db/dbdataemoji";
+import { MatrixMessageProcessor } from "../src/matrixmessageprocessor";
+
+// we are a test file and thus need those
+/* tslint:disable:no-unused-expression max-file-line-count no-any */
+
+const expect = Chai.expect;
+
+const mxClient = {
+    getStateEvent: async (roomId, stateType, _) => {
+        if (stateType === "m.room.power_levels") {
+            return {
+                notifications: {
+                    room: 50,
+                },
+                users: {
+                    "@nopower:localhost": 0,
+                    "@power:localhost": 100,
+                },
+            };
+        }
+        return null;
+    },
+};
+
+const bot = {
+    GetEmojiByMxc: async (mxc: string): Promise<DbEmoji> => {
+        if (mxc === "mxc://real_emote:localhost") {
+            const emoji = new DbEmoji();
+            emoji.Name = "real_emote";
+            emoji.EmojiId = "123456";
+            emoji.Animated = false;
+            emoji.MxcUrl = mxc;
+            return emoji;
+        }
+        throw new Error("Couldn't fetch from store");
+    },
+} as DiscordBot;
+
+function getPlainMessage(msg: string, msgtype: string = "m.text") {
+    return {
+        body: msg,
+        msgtype,
+    };
+}
+
+function getHtmlMessage(msg: string, msgtype: string = "m.text") {
+    return {
+        body: msg,
+        formatted_body: msg,
+        msgtype,
+    };
+}
+
+describe("MatrixMessageProcessor", () => {
+    describe("FormatMessage / body / simple", () => {
+        it("leaves blank stuff untouched", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("hello world!");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("hello world!");
+        });
+        it("escapes simple stuff", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("hello *world* how __are__ you?");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("hello \\*world\\* how \\_\\_are\\_\\_ you?");
+        });
+        it("escapes more complex stuff", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("wow \\*this\\* is cool");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("wow \\\\\\*this\\\\\\* is cool");
+        });
+    });
+    describe("FormatMessage / formatted_body / simple", () => {
+        it("leaves blank stuff untouched", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("hello world!");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("hello world!");
+        });
+        it("un-escapes simple stuff", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("foxes &amp; foxes");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("foxes & foxes");
+        });
+        it("converts italic formatting", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("this text is <em>italic</em> and so is <i>this one</i>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("this text is *italic* and so is *this one*");
+        });
+        it("converts bold formatting", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("wow some <b>bold</b> and <strong>more</strong> boldness!");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("wow some **bold** and **more** boldness!");
+        });
+        it("converts underline formatting", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("to be <u>underlined</u> or not to be?");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("to be __underlined__ or not to be?");
+        });
+        it("converts strike formatting", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("does <del>this text</del> exist?");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("does ~~this text~~ exist?");
+        });
+        it("converts code", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("WOW this is <code>some awesome</code> code");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("WOW this is `some awesome` code");
+        });
+        it("converts multiline-code", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<p>here</p><pre><code>is\ncode\n</code></pre><p>yay</p>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("here```\nis\ncode\n```yay");
+        });
+        it("converts multiline language code", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage(`<p>here</p>
+<pre><code class="language-js">is
+code
+</code></pre>
+<p>yay</p>`);
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("here```js\nis\ncode\n```yay");
+        });
+        it("handles linebreaks", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("line<br>break");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("line\nbreak");
+        });
+        it("handles <hr>", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("test<hr>foxes");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("test\n----------\nfoxes");
+        });
+    });
+    describe("FormatMessage / formatted_body / complex", () => {
+        it("html unescapes stuff inside of code", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<code>is &lt;em&gt;italic&lt;/em&gt;?</code>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("`is <em>italic</em>?`");
+        });
+        it("html unescapes inside of pre", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<pre><code>wow &amp;</code></pre>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("```\nwow &```");
+        });
+        it("doesn't parse inside of code", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<code>*yay*</code>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("`*yay*`");
+        });
+        it("doesn't parse inside of pre", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<pre><code>*yay*</code></pre>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("```\n*yay*```");
+        });
+        it("parses new lines", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<em>test</em><br><strong>ing</strong>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("*test*\n**ing**");
+        });
+        it("drops mx-reply", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<mx-reply><blockquote>message</blockquote></mx-reply>test reply");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("test reply");
+        });
+        it("parses links", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<a href=\"http://example.com\">link</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("[link](http://example.com)");
+        });
+        it("parses links with same content", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<a href=\"http://example.com\">http://example.com</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("http://example.com");
+        });
+        it("doesn't discord-escape links", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<a href=\"http://example.com/_blah_/\">link</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("[link](http://example.com/_blah_/)");
+        });
+        it("doesn't discord-escape links with same content", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<a href=\"http://example.com/_blah_/\">http://example.com/_blah_/</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("http://example.com/_blah_/");
+        });
+    });
+    describe("FormatMessage / formatted_body / discord", () => {
+        it("Parses user pills", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const member = new MockMember("12345", "TestUsername", guild);
+            guild.members.set("12345", member);
+            const msg = getHtmlMessage("<a href=\"https://matrix.to/#/@_discord_12345:localhost\">TestUsername</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("<@12345>");
+        });
+        it("Ignores invalid user pills", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const member = new MockMember("12345", "TestUsername", guild);
+            guild.members.set("12345", member);
+            const msg = getHtmlMessage("<a href=\"https://matrix.to/#/@_discord_789:localhost\">TestUsername</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("[TestUsername](https://matrix.to/#/@_discord_789:localhost)");
+        });
+        it("Parses channel pills", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const channel = new MockChannel("12345", guild, "text", "SomeChannel");
+            guild.channels.set("12345", channel as any);
+            const msg = getHtmlMessage("<a href=\"https://matrix.to/#/#_discord_1234_12345:" +
+                "localhost\">#SomeChannel</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("<#12345>");
+        });
+        it("Handles invalid channel pills", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const channel = new MockChannel("12345", guild, "text", "SomeChannel");
+            guild.channels.set("12345", channel as any);
+            const msg = getHtmlMessage("<a href=\"https://matrix.to/#/#_discord_1234_789:localhost\">#SomeChannel</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("https://matrix.to/#/#_discord_1234_789:localhost");
+        });
+        it("Handles external channel pills", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<a href=\"https://matrix.to/#/#matrix:matrix.org\">#SomeChannel</a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("https://matrix.to/#/#matrix:matrix.org");
+        });
+        it("Ignores links without href", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<a><em>yay?</em></a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("*yay?*");
+        });
+        it("Ignores links with non-matrix href", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<a href=\"http://example.com\"><em>yay?</em></a>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("[*yay?*](http://example.com)");
+        });
+    });
+    describe("FormatMessage / formatted_body / emoji", () => {
+        it("Inserts emoji by name", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const emoji = new MockEmoji("123456", "test_emoji");
+            guild.emojis.set("123456", emoji);
+            const msg = getHtmlMessage("<img alt=\"test_emoji\">");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("<:test_emoji:123456>");
+        });
+        it("Inserts emojis by mxc url", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const emoji = new MockEmoji("123456", "test_emoji");
+            guild.emojis.set("123456", emoji);
+            const msg = getHtmlMessage("<img src=\"mxc://real_emote:localhost\">");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("<:test_emoji:123456>");
+        });
+        it("ignores unknown mxc urls", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const emoji = new MockEmoji("123456", "test_emoji");
+            guild.emojis.set("123456", emoji);
+            const msg = getHtmlMessage("<img alt=\"yay\" src=\"mxc://unreal_emote:localhost\">");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("yay");
+        });
+        it("ignores with no alt / title, too", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const emoji = new MockEmoji("123456", "test_emoji");
+            guild.emojis.set("123456", emoji);
+            const msg = getHtmlMessage("<img>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("");
+        });
+    });
+    describe("FormatMessage / formatted_body / matrix", () => {
+        it("escapes @everyone", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("hey @everyone");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("hey @\u200Beveryone");
+        });
+        it("escapes @here", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("hey @here");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("hey @\u200Bhere");
+        });
+        it("converts @room to @here, if sufficient power", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("hey @room");
+            const params = {
+                mxClient,
+                roomId: "!123456:localhost",
+                userId: "@power:localhost",
+            };
+            const result = await mp.FormatMessage(msg, guild as any, params as any);
+            expect(result).is.equal("hey @here");
+        });
+        it("ignores @room to @here conversion, if insufficient power", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("hey @room");
+            const params = {
+                mxClient,
+                roomId: "!123456:localhost",
+                userId: "@nopower:localhost",
+            };
+            const result = await mp.FormatMessage(msg, guild as any, params as any);
+            expect(result).is.equal("hey @room");
+        });
+        it("handles /me for normal names", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("floofs", "m.emote");
+            const params = {
+                displayname: "fox",
+            };
+            const result = await mp.FormatMessage(msg, guild as any, params as any);
+            expect(result).is.equal("_fox floofs_");
+        });
+        it("handles /me for short names", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("floofs", "m.emote");
+            const params = {
+                displayname: "f",
+            };
+            const result = await mp.FormatMessage(msg, guild as any, params as any);
+            expect(result).is.equal("_floofs_");
+        });
+        it("handles /me for long names", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getPlainMessage("floofs", "m.emote");
+            const params = {
+                displayname: "foxfoxfoxfoxfoxfoxfoxfoxfoxfoxfoxfox",
+            };
+            const result = await mp.FormatMessage(msg, guild as any, params as any);
+            expect(result).is.equal("_floofs_");
+        });
+    });
+    describe("FormatMessage / formatted_body / blockquotes", () => {
+        it("parses single blockquotes", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<blockquote>hey</blockquote>there");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("> hey\n\nthere");
+        });
+        it("parses double blockquotes", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<blockquote><blockquote>hey</blockquote>you</blockquote>there");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("> > hey\n> \n> you\n\nthere");
+        });
+        it("parses blockquotes with <p>", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<blockquote>\n<p>spoky</p>\n</blockquote>\n<p>test</p>\n");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("> spoky\n\ntest");
+        });
+        it("parses double blockquotes with <p>", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage(`<blockquote>
+<blockquote>
+<p>spoky</p>
+</blockquote>
+<p>testing</p>
+</blockquote>
+<p>test</p>
+`);
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("> > spoky\n> \n> testing\n\ntest");
+        });
+    });
+    describe("FormatMessage / formatted_body / lists", () => {
+        it("parses simple unordered lists", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage(`<p>soru</p>
+<ul>
+<li>test</li>
+<li>ing</li>
+</ul>
+<p>more</p>
+`);
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("soru\n● test\n● ing\n\nmore");
+        });
+        it("parses nested unordered lists", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage(`<p>foxes</p>
+<ul>
+<li>awesome</li>
+<li>floofy
+<ul>
+<li>fur</li>
+<li>tail</li>
+</ul>
+</li>
+</ul>
+<p>yay!</p>
+`);
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("foxes\n● awesome\n● floofy\n    ○ fur\n    ○ tail\n\nyay!");
+        });
+        it("parses more nested unordered lists", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage(`<p>foxes</p>
+<ul>
+<li>awesome</li>
+<li>floofy
+<ul>
+<li>fur</li>
+<li>tail</li>
+</ul>
+</li>
+<li>cute</li>
+</ul>
+<p>yay!</p>
+`);
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("foxes\n● awesome\n● floofy\n    ○ fur\n    ○ tail\n● cute\n\nyay!");
+        });
+    });
+    it("parses simple ordered lists", async () => {
+        const mp = new MatrixMessageProcessor(bot);
+        const guild = new MockGuild("1234");
+        const msg = getHtmlMessage(`<p>oookay</p>
+<ol>
+<li>test</li>
+<li>test more</li>
+</ol>
+<p>ok?</p>
+`);
+        const result = await mp.FormatMessage(msg, guild as any);
+        expect(result).is.equal("oookay\n1. test\n2. test more\n\nok?");
+    });
+    it("parses nested ordered lists", async () => {
+        const mp = new MatrixMessageProcessor(bot);
+        const guild = new MockGuild("1234");
+        const msg = getHtmlMessage(`<p>and now</p>
+<ol>
+<li>test</li>
+<li>test more
+<ol>
+<li>and more</li>
+<li>more?</li>
+</ol>
+</li>
+<li>done!</li>
+</ol>
+<p>ok?</p>
+`);
+        const result = await mp.FormatMessage(msg, guild as any);
+        expect(result).is.equal("and now\n1. test\n2. test more\n    1. and more\n    2. more?\n3. done!\n\nok?");
+    });
+    it("parses ordered lists with different start", async () => {
+        const mp = new MatrixMessageProcessor(bot);
+        const guild = new MockGuild("1234");
+        const msg = getHtmlMessage(`<ol start="5">
+<li>test</li>
+<li>test more</li>
+</ol>`);
+        const result = await mp.FormatMessage(msg, guild as any);
+        expect(result).is.equal("\n5. test\n6. test more");
+    });
+    it("parses ul in ol", async () => {
+        const mp = new MatrixMessageProcessor(bot);
+        const guild = new MockGuild("1234");
+        const msg = getHtmlMessage(`<ol>
+<li>test</li>
+<li>test more
+<ul>
+<li>asdf</li>
+<li>jklö</li>
+</ul>
+</li>
+</ol>`);
+        const result = await mp.FormatMessage(msg, guild as any);
+        expect(result).is.equal("\n1. test\n2. test more\n    ○ asdf\n    ○ jklö");
+    });
+    it("parses ol in ul", async () => {
+        const mp = new MatrixMessageProcessor(bot);
+        const guild = new MockGuild("1234");
+        const msg = getHtmlMessage(`<ul>
+<li>test</li>
+<li>test more
+<ol>
+<li>asdf</li>
+<li>jklö</li>
+</ol>
+</li>
+</ul>`);
+        const result = await mp.FormatMessage(msg, guild as any);
+        expect(result).is.equal("\n● test\n● test more\n    1. asdf\n    2. jklö");
+    });
+});
diff --git a/test/test_util.ts b/test/test_util.ts
index 9284942d14f4dcd3438be6df2cd26371353bf202..5003a4fd7d09c66dc5d261becfeca75f2931a782 100644
--- a/test/test_util.ts
+++ b/test/test_util.ts
@@ -125,30 +125,6 @@ describe("Util", () => {
             }
         });
     });
-    describe("GetReplyFromReplyBody", () => {
-        it("Should get a reply from the body", () => {
-            const reply = Util.GetReplyFromReplyBody(`> <@alice:example.org> This is the original body
-
-This is where the reply goes`);
-            expect(reply).to.equal("This is where the reply goes");
-        });
-        it("Should get a multi-line reply from the body", () => {
-            const reply = Util.GetReplyFromReplyBody(`> <@alice:example.org> This is the original body
-
-This is where the reply goes and
-there are even more lines here.`);
-            expect(reply).to.equal("This is where the reply goes and\nthere are even more lines here.");
-        });
-        it("Should get empty string from an empty reply", () => {
-            const reply = Util.GetReplyFromReplyBody(`> <@alice:example.org> This is the original body
-`);
-            expect(reply).to.equal("");
-        });
-        it("Should return body if no reply found", () => {
-            const reply = Util.GetReplyFromReplyBody("Test\nwith\nhalfy");
-            expect(reply).to.equal("Test\nwith\nhalfy");
-        });
-    });
     describe("NumberToHTMLColor", () => {
         it("Should handle valid colors", () => {
             const COLOR = 0xdeadaf;