diff --git a/config/config.sample.yaml b/config/config.sample.yaml
index 10f6d2fb97e7f9ca24fff97f962683894245c11b..d000b2d3902c8bcc25326d68c1ac6426b613a4f1 100644
--- a/config/config.sample.yaml
+++ b/config/config.sample.yaml
@@ -31,6 +31,7 @@ bridge:
   disableJoinLeaveNotifications: false
 # Authentication configuration for the discord bot.
 auth:
+  # This MUST be a string (wrapped in quotes)
   clientID: "12345"
   botToken: "foobar"
 logging:
@@ -85,10 +86,12 @@ channel:
 limits:
     # Delay in milliseconds between discord users joining a room.
     roomGhostJoinDelay: 6000
-    # Delay in milliseconds before sending messages to discord to avoid echos.
-    # (Copies of a sent message may arrive from discord before we've
+    # Lock timeout in milliseconds before seinding messages to discord to avoid
+    # echos. Default is rather high as the lock will most likely time out
+    # before anyways.
+    # echos = (Copies of a sent message may arrive from discord before we've
     # fininished handling it, causing us to echo it back to the room)
-    discordSendDelay: 750
+    discordSendDelay: 1500
 ghosts:
     # Pattern for the ghosts nick, available is :nick, :username, :tag and :id
     nickPattern: ":nick"
diff --git a/docs/howto.md b/docs/howto.md
index cbdee791fc771018dbbf0481cb07da5d15c3357b..f43902f5b683228d426dabb541354ff0b4538d32 100644
--- a/docs/howto.md
+++ b/docs/howto.md
@@ -15,7 +15,7 @@ is formatted as https://discordapp.com/channels/``guildid``/``channelid``
 
 * The ``adminme`` script is provided to set Admin/Moderator or any other custom power level to a specific user.
 * e.g. To set Alice to Admin on her ``example.com`` HS on default config. (``config.yaml``)
-  * ``npm run adminme -- -r '!AbcdefghijklmnopqR:example.com' -u '@Alice:example.com' -p '100'``
+  * ``npm run adminme -- -m '!AbcdefghijklmnopqR:example.com' -u '@Alice:example.com' -p '100'``
   * Run ``npm run adminme -- -h`` for usage.
 
 Please note that `!AbcdefghijklmnopqR:example.com` is the internal room id and will always begin with `!`.
diff --git a/package-lock.json b/package-lock.json
index 788bc7522b7a956aa2ede8ee4ff93ae294860217..dee36c57af602cdad92f50aeef6afe7b0b56e117 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -957,12 +957,11 @@
       "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
     },
     "discord-markdown": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/discord-markdown/-/discord-markdown-2.0.0.tgz",
-      "integrity": "sha512-uiDNjFrMjAFK50Q+z7qI4siF6U3XBx6gZ6GywjrQnoiBmWK5d/wJDoIT051bOD4xXBHuSENXsReF9zBvbCjDGQ==",
+      "version": "git://github.com/Sorunome/discord-markdown.git#5086d4ccb10a90bcdc7656606f5b3f5430bffee9",
+      "from": "git://github.com/Sorunome/discord-markdown.git#5086d4ccb10a90bcdc7656606f5b3f5430bffee9",
       "requires": {
         "highlight.js": "^9.13.1",
-        "simple-markdown": "^0.4.2"
+        "simple-markdown": "^0.4.4"
       }
     },
     "discord.js": {
@@ -1757,9 +1756,9 @@
       "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
     },
     "highlight.js": {
-      "version": "9.13.1",
-      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz",
-      "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A=="
+      "version": "9.15.8",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.8.tgz",
+      "integrity": "sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA=="
     },
     "hosted-git-info": {
       "version": "2.7.1",
@@ -2792,9 +2791,9 @@
       "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==",
+      "version": "1.1.16",
+      "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.16.tgz",
+      "integrity": "sha512-cfqTZIYDdp5cGh3NvCD5dcEDP7hfyni7WgyFacmDynLlIZaF3GVlRk8yMARhWp/PobWt1KaCV8VKdP5LKWiVbg==",
       "requires": {
         "he": "1.1.1"
       }
diff --git a/package.json b/package.json
index 4c8c220a6455a2767b8e2488d4a3906e80d0429c..0401f60b061b4eeb160d2504d67caad0be6afbd2 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
     "coverage": "tsc && nyc mocha",
     "build": "tsc",
     "start": "npm run-script build && node ./build/src/discordas.js -p 9005 -c config.yaml",
+    "debug": "npm run-script build && node --inspect ./build/src/discordas.js -p 9005 -c config.yaml",
     "addbot": "node ./build/tools/addbot.js",
     "adminme": "node ./build/tools/adminme.js",
     "usertool": "node ./build/tools/userClientTools.js",
@@ -39,14 +40,14 @@
     "better-sqlite3": "^5.0.1",
     "command-line-args": "^4.0.7",
     "command-line-usage": "^4.1.0",
-    "discord-markdown": "^2.0.0",
+    "discord-markdown": "git://github.com/Sorunome/discord-markdown#5086d4ccb10a90bcdc7656606f5b3f5430bffee9",
     "discord.js": "^11.5.1",
     "escape-html": "^1.0.3",
     "escape-string-regexp": "^1.0.5",
     "js-yaml": "^3.13.1",
     "matrix-bot-sdk": "0.4.0-beta.3",
     "mime": "^1.6.0",
-    "node-html-parser": "^1.1.11",
+    "node-html-parser": "^1.1.15",
     "pg-promise": "^8.5.1",
     "prom-client": "^11.3.0",
     "tslint": "^5.11.0",
diff --git a/src/bot.ts b/src/bot.ts
index 23457ab9dc0c631b03a607da97f3712505020ab5..9ed386093e736de6cd2d136ead790e002f1c5bac 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -38,6 +38,7 @@ import { IMatrixEvent, IMatrixMediaInfo } from "./matrixtypes";
 import { Appservice, Intent } from "matrix-bot-sdk";
 import { DiscordCommandHandler } from "./discordcommandhandler";
 import { MetricPeg } from "./metrics";
+import { Lock } from "./structures/lock";
 
 const log = new Log("DiscordBot");
 
@@ -49,6 +50,7 @@ const MATRIX_ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org
 class ChannelLookupResult {
     public channel: Discord.TextChannel;
     public botUser: boolean;
+    public canSendEmbeds: boolean;
 }
 
 interface IThirdPartyLookupField {
@@ -82,8 +84,7 @@ export class DiscordBot {
 
     /* Handles messages queued up to be sent to matrix from discord. */
     private discordMessageQueue: { [channelId: string]: Promise<void> };
-    private channelLocks: Map<string, {i: NodeJS.Timeout|null, r: (() => void)|null}>;
-    private channelLockPromises: Map<string, Promise<{}>>;
+    private channelLock: Lock<string>;
     constructor(
         private config: DiscordBridgeConfig,
         private bridge: Appservice,
@@ -106,8 +107,7 @@ export class DiscordBot {
         // init vars
         this.sentMessages = [];
         this.discordMessageQueue = {};
-        this.channelLocks = new Map();
-        this.channelLockPromises = new Map();
+        this.channelLock = new Lock(this.config.limits.discordSendDelay);
         this.lastEventIds = {};
     }
 
@@ -139,43 +139,6 @@ export class DiscordBot {
         return this.provisioner;
     }
 
-    public lockChannel(channel: Discord.Channel) {
-        if (this.channelLocks.has(channel.id)) {
-            return;
-        }
-
-        this.channelLocks[channel.id] = {i: null, r: null, p: null};
-        const p = new Promise<{}>((resolve) => {
-            const i = setTimeout(() => {
-                log.warn(`Lock on channel ${channel.id} expired. Discord is lagging behind?`);
-                this.unlockChannel(channel);
-            }, this.config.limits.discordSendDelay);
-            const o = Object.assign({r: resolve, i, p: null}, this.channelLocks.get(channel.id) || {});
-            this.channelLocks.set(channel.id, o);
-        });
-        this.channelLockPromises.set(channel.id, p);
-    }
-
-    public unlockChannel(channel: Discord.Channel) {
-        if (!this.channelLocks.has(channel.id)) {
-            return;
-        }
-        const lock = this.channelLocks.get(channel.id)!;
-        if (lock.i !== null) {
-            lock.r!();
-            clearTimeout(lock.i);
-        }
-        this.channelLocks.delete(channel.id);
-        this.channelLockPromises.delete(channel.id);
-    }
-
-    public async waitUnlock(channel: Discord.Channel) {
-        const promise = this.channelLockPromises.get(channel.id);
-        if (promise) {
-            await promise;
-        }
-    }
-
     public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.User, webhookID?: string): Intent {
         if (webhookID) {
             // webhookID and user IDs are the same, they are unique, so no need to prefix _webhook_
@@ -239,7 +202,7 @@ export class DiscordBot {
 
         client.on("messageDelete", async (msg: Discord.Message) => {
             try {
-                await this.waitUnlock(msg.channel);
+                await this.channelLock.wait(msg.channel.id);
                 this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel);
                 this.discordMessageQueue[msg.channel.id] = (async () => {
                     await (this.discordMessageQueue[msg.channel.id] || Promise.resolve());
@@ -260,7 +223,7 @@ export class DiscordBot {
                 msgs.forEach((msg) => {
                     promiseArr.push(async () => {
                         try {
-                            await this.waitUnlock(msg.channel);
+                            await this.channelLock.wait(msg.channel.id);
                             this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel);
                             await this.DeleteDiscordMessage(msg);
                         } catch (err) {
@@ -275,7 +238,7 @@ export class DiscordBot {
         });
         client.on("messageUpdate", async (oldMessage: Discord.Message, newMessage: Discord.Message) => {
             try {
-                await this.waitUnlock(newMessage.channel);
+                await this.channelLock.wait(newMessage.channel.id);
                 this.clientFactory.bindMetricsToChannel(newMessage.channel as Discord.TextChannel);
                 this.discordMessageQueue[newMessage.channel.id] = (async () => {
                     await (this.discordMessageQueue[newMessage.channel.id] || Promise.resolve());
@@ -292,7 +255,7 @@ export class DiscordBot {
         client.on("message", async (msg: Discord.Message) => {
             try {
                 MetricPeg.get.registerRequest(msg.id);
-                await this.waitUnlock(msg.channel);
+                await this.channelLock.wait(msg.channel.id);
                 this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel);
                 this.discordMessageQueue[msg.channel.id] = (async () => {
                     await (this.discordMessageQueue[msg.channel.id] || Promise.resolve());
@@ -399,6 +362,7 @@ export class DiscordBot {
                 const lookupResult = new ChannelLookupResult();
                 lookupResult.channel = channel as Discord.TextChannel;
                 lookupResult.botUser = this.bot.user.id === client.user.id;
+                lookupResult.canSendEmbeds = client.user.bot; // only bots can send embeds
                 return lookupResult;
             }
             throw new Error(`Channel "${room}" not found`);
@@ -416,10 +380,10 @@ export class DiscordBot {
         if (!msg) {
             return;
         }
-        this.lockChannel(channel);
+        this.channelLock.set(channel.id);
         const res = await channel.send(msg);
         await this.StoreMessagesSent(res, channel, event);
-        this.unlockChannel(channel);
+        this.channelLock.release(channel.id);
     }
 
     public async send(
@@ -450,19 +414,56 @@ export class DiscordBot {
             }
         }
         try {
-            this.lockChannel(chan);
-            if (!botUser) {
-                // NOTE: Don't send replies to discord if we are a puppet.
+            this.channelLock.set(chan.id);
+            if (!roomLookup.canSendEmbeds) {
+                // NOTE: Don't send replies to discord if we are a puppet user.
+                let addText = "";
+                if (embedSet.replyEmbed) {
+                    for (const line of embedSet.replyEmbed.description!.split("\n")) {
+                        addText += "\n> " + line;
+                    }
+                }
+                msg = await chan.send(embed.description + addText, opts);
+            } else if (!botUser) {
+                if (embedSet.imageEmbed || embedSet.replyEmbed) {
+                    let sendEmbed = new Discord.RichEmbed();
+                    if (embedSet.imageEmbed) {
+                        if (!embedSet.replyEmbed) {
+                            sendEmbed = embedSet.imageEmbed;
+                        } else {
+                            sendEmbed.setImage(embedSet.imageEmbed.image!.url);
+                        }
+                    }
+                    if (embedSet.replyEmbed) {
+                        if (!embedSet.imageEmbed) {
+                            sendEmbed = embedSet.replyEmbed;
+                        } else {
+                            sendEmbed.addField("Replying to", embedSet.replyEmbed!.author!.name);
+                            sendEmbed.addField("Reply text", embedSet.replyEmbed.description);
+                        }
+                    }
+                    opts.embed = sendEmbed;
+                }
                 msg = await chan.send(embed.description, opts);
             } else if (hook) {
                 MetricPeg.get.remoteCall("hook.send");
+                const embeds: Discord.RichEmbed[] = [];
+                if (embedSet.imageEmbed) {
+                    embeds.push(embedSet.imageEmbed);
+                }
+                if (embedSet.replyEmbed) {
+                    embeds.push(embedSet.replyEmbed);
+                }
                 msg = await hook.send(embed.description, {
                     avatarURL: embed!.author!.icon_url,
-                    embeds: embedSet.replyEmbed ? [embedSet.replyEmbed] : undefined,
+                    embeds,
                     files: opts.file ? [opts.file] : undefined,
                     username: embed!.author!.name,
                 } as Discord.WebhookMessageOptions);
             } else {
+                if (embedSet.imageEmbed) {
+                    embed.setImage(embedSet.imageEmbed.image!.url);
+                }
                 if (embedSet.replyEmbed) {
                     embed.addField("Replying to", embedSet.replyEmbed!.author!.name);
                     embed.addField("Reply text", embedSet.replyEmbed.description);
@@ -472,7 +473,7 @@ export class DiscordBot {
             }
             // Don't block on this.
             this.StoreMessagesSent(msg, chan, event).then(() => {
-                this.unlockChannel(chan);
+                this.channelLock.release(chan.id);
             }).catch(() => {
                 log.warn("Failed to store sent message for ", event.event_id);
             });
@@ -503,9 +504,9 @@ export class DiscordBot {
 
             const msg = await chan.fetchMessage(storeEvent.DiscordId);
             try {
-                this.lockChannel(msg.channel);
+                this.channelLock.set(msg.channel.id);
                 await msg.delete();
-                this.unlockChannel(msg.channel);
+                this.channelLock.release(msg.channel.id);
                 log.info(`Deleted message`);
             } catch (ex) {
                 log.warn(`Failed to delete message`, ex);
@@ -658,12 +659,12 @@ export class DiscordBot {
                   /* tslint:disable-next-line no-any */
               } as any, // XXX: Discord.js typings are wrong.
                 `Unbanned.`);
-            this.lockChannel(botChannel);
+            this.channelLock.set(botChannel.id);
             res = await botChannel.send(
                 `${kickee} was unbanned from this channel by ${kicker}.`,
             ) as Discord.Message;
             this.sentMessages.push(res.id);
-            this.unlockChannel(botChannel);
+            this.channelLock.release(botChannel.id);
             return;
         }
         const existingPerms = tchan.memberPermissions(kickee);
@@ -672,13 +673,13 @@ export class DiscordBot {
             return;
         }
         const word = `${kickban === "ban" ? "banned" : "kicked"}`;
-        this.lockChannel(botChannel);
+        this.channelLock.set(botChannel.id);
         res = await botChannel.send(
             `${kickee} was ${word} from this channel by ${kicker}.`
             + (reason ? ` Reason: ${reason}` : ""),
         ) as Discord.Message;
         this.sentMessages.push(res.id);
-        this.unlockChannel(botChannel);
+        this.channelLock.release(botChannel.id);
         log.info(`${word} ${kickee}`);
 
         await tchan.overwritePermissions(kickee,
diff --git a/src/config.ts b/src/config.ts
index 94c5a91fc34037fe663c86226b58ab61f13e5b80..756595085475055c2cb06386506bf666c3889efa 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -135,7 +135,7 @@ export class DiscordBridgeConfigChannelDeleteOptions {
 
 class DiscordBridgeConfigLimits {
     public roomGhostJoinDelay: number = 6000;
-    public discordSendDelay: number = 750;
+    public discordSendDelay: number = 1500;
 }
 
 export class LoggingFile {
diff --git a/src/discordmessageprocessor.ts b/src/discordmessageprocessor.ts
index 29849fcac17724e4cc19f21c649dadd5ddc9f2bb..097b674bae061a9d9c8282b764fa23502b294fad 100644
--- a/src/discordmessageprocessor.ts
+++ b/src/discordmessageprocessor.ts
@@ -48,6 +48,10 @@ export class DiscordMessageProcessorResult {
     public msgtype: string;
 }
 
+interface ISpoilerNode {
+    content: string;
+}
+
 interface IDiscordNode {
     id: string;
 }
@@ -77,6 +81,7 @@ export class DiscordMessageProcessor {
         // as else it'll HTML escape the result of the discord syntax
         let contentPostmark = markdown.toHTML(content, {
             discordCallback: this.getDiscordParseCallbacksHTML(msg),
+            noExtraSpanTags: true,
         });
 
 
@@ -85,6 +90,7 @@ export class DiscordMessageProcessor {
             discordCallback: this.getDiscordParseCallbacks(msg),
             discordOnly: true,
             escapeHTML: false,
+            noExtraSpanTags: true,
         });
         content = this.InsertEmbeds(content, msg);
         content = await this.InsertMxcImages(content, msg);
@@ -144,6 +150,7 @@ export class DiscordMessageProcessor {
                     discordCallback: this.getDiscordParseCallbacks(msg),
                     discordOnly: true,
                     escapeHTML: false,
+                    noExtraSpanTags: true,
                 });
             }
             if (embed.fields) {
@@ -153,6 +160,7 @@ export class DiscordMessageProcessor {
                         discordCallback: this.getDiscordParseCallbacks(msg),
                         discordOnly: true,
                         escapeHTML: false,
+                        noExtraSpanTags: true,
                     });
                 }
             }
@@ -164,6 +172,7 @@ export class DiscordMessageProcessor {
                     discordCallback: this.getDiscordParseCallbacks(msg),
                     discordOnly: true,
                     escapeHTML: false,
+                    noExtraSpanTags: true,
                 });
             }
             content += embedContent;
@@ -191,6 +200,7 @@ export class DiscordMessageProcessor {
                 embedContent += markdown.toHTML(embed.description, {
                     discordCallback: this.getDiscordParseCallbacksHTML(msg),
                     embed: true,
+                    noExtraSpanTags: true,
                 }) + "</p>";
             }
             if (embed.fields) {
@@ -199,6 +209,7 @@ export class DiscordMessageProcessor {
                     embedContent += markdown.toHTML(field.value, {
                         discordCallback: this.getDiscordParseCallbacks(msg),
                         embed: true,
+                        noExtraSpanTags: true,
                     }) + "</p>";
                 }
             }
@@ -211,6 +222,7 @@ export class DiscordMessageProcessor {
                 embedContent += markdown.toHTML(embed.footer.text, {
                     discordCallback: this.getDiscordParseCallbacksHTML(msg),
                     embed: true,
+                    noExtraSpanTags: true,
                 }) + "</p>";
             }
             content += embedContent;
@@ -229,6 +241,15 @@ export class DiscordMessageProcessor {
         return `<a href="${MATRIX_TO_LINK}${escapeHtml(memberId)}">${escapeHtml(memberName)}</a>`;
     }
 
+    public InsertSpoiler(node: ISpoilerNode, html: boolean = false): string {
+        // matrix spoilers are still in MSC stage
+        // see https://github.com/matrix-org/matrix-doc/pull/2010
+        if (!html) {
+            return `(Spoiler: ${node.content})`;
+        }
+        return `<span data-mx-spoiler>${node.content}</span>`;
+    }
+
     public InsertChannel(node: IDiscordNode): string {
         // unfortunately these callbacks are sync, so we flag our channel with some special stuff
         // and later on grab the real channel pill async
@@ -332,6 +353,7 @@ export class DiscordMessageProcessor {
             everyone: (_) => this.InsertRoom(msg, "@everyone"),
             here: (_) => this.InsertRoom(msg, "@here"),
             role: (node) => this.InsertRole(node, msg),
+            spoiler: (node) => this.InsertSpoiler(node),
             user: (node) => this.InsertUser(node, msg),
         };
     }
@@ -343,6 +365,7 @@ export class DiscordMessageProcessor {
             everyone: (_) => this.InsertRoom(msg, "@everyone"),
             here: (_) => this.InsertRoom(msg, "@here"),
             role: (node) => this.InsertRole(node, msg, true),
+            spoiler: (node) => this.InsertSpoiler(node, true),
             user: (node) => this.InsertUser(node, msg, true),
         };
     }
diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts
index c0305aeb713997c906ee1f4c4d52437efe91b502..cdc6b5a952f71f32e221638442defe00f76ce266 100644
--- a/src/matrixeventprocessor.ts
+++ b/src/matrixeventprocessor.ts
@@ -55,6 +55,7 @@ export class MatrixEventProcessorOpts {
 export interface IMatrixEventProcessorResult {
     messageEmbed: Discord.RichEmbed;
     replyEmbed?: Discord.RichEmbed;
+    imageEmbed?: Discord.RichEmbed;
 }
 
 export class MatrixEventProcessor {
@@ -179,11 +180,13 @@ export class MatrixEventProcessor {
 
         const embedSet = await this.EventToEmbed(event, chan);
         const opts: Discord.MessageOptions = {};
-        const file = await this.HandleAttachment(event, mxClient);
+        const file = await this.HandleAttachment(event, mxClient, roomLookup.canSendEmbeds);
         if (typeof(file) === "string") {
             embedSet.messageEmbed.description += " " + file;
+        } else if ((file as Discord.FileOptions).name && (file as Discord.FileOptions).attachment) {
+            opts.file = file as Discord.FileOptions;
         } else {
-            opts.file = file;
+            embedSet.imageEmbed = file as Discord.RichEmbed;
         }
 
         await this.discord.send(embedSet, opts, roomLookup, event);
@@ -293,7 +296,11 @@ export class MatrixEventProcessor {
         };
     }
 
-    public async HandleAttachment(event: IMatrixEvent, mxClient: MatrixClient): Promise<string|Discord.FileOptions> {
+    public async HandleAttachment(
+        event: IMatrixEvent,
+        mxClient: MatrixClient,
+        sendEmbeds: boolean = false,
+    ): Promise<string|Discord.FileOptions|Discord.RichEmbed> {
         if (!this.HasAttachment(event)) {
             return "";
         }
@@ -305,7 +312,7 @@ export class MatrixEventProcessor {
         if (!event.content.info) {
             // Fractal sends images without an info, which is technically allowed
             // but super unhelpful:  https://gitlab.gnome.org/World/fractal/issues/206
-            event.content.info = {size: 0};
+            event.content.info = {mimetype: "", size: 0};
         }
 
         if (!event.content.url) {
@@ -326,6 +333,10 @@ export class MatrixEventProcessor {
                 } as Discord.FileOptions;
             }
         }
+        if (sendEmbeds && event.content.info.mimetype.split("/")[0] === "image") {
+            return new Discord.RichEmbed()
+                .setImage(url);
+        }
         return `[${name}](${url})`;
     }
 
diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts
index cdaf95f7dcf9a3a79327e716657e62d1276d97ba..3f32ba89243f214f15868b532a3416f276bbf559 100644
--- a/src/matrixmessageprocessor.ts
+++ b/src/matrixmessageprocessor.ts
@@ -246,6 +246,21 @@ export class MatrixMessageProcessor {
         return msg;
     }
 
+    private async parseSpanContent(node: Parser.HTMLElement): Promise<string> {
+        const content = await this.walkChildNodes(node);
+        const attrs = node.attributes;
+        // matrix spoilers are still in MSC stage
+        // see https://github.com/matrix-org/matrix-doc/pull/2010
+        if (attrs["data-mx-spoiler"] !== undefined) {
+            const spoilerReason = attrs["data-mx-spoiler"];
+            if (spoilerReason) {
+                return `(${spoilerReason})||${content}||`;
+            }
+            return `||${content}||`;
+        }
+        return content;
+    }
+
     private async parseUlContent(node: Parser.HTMLElement): Promise<string> {
         this.listDepth++;
         const entries = await this.arrayChildNodes(node, ["li"]);
@@ -353,6 +368,8 @@ export class MatrixMessageProcessor {
                 case "h6":
                     const level = parseInt(nodeHtml.tagName[1], 10);
                     return `**${"#".repeat(level)} ${await this.walkChildNodes(nodeHtml)}**\n`;
+                case "span":
+                    return await this.parseSpanContent(nodeHtml);
                 default:
                     return await this.walkChildNodes(nodeHtml);
             }
diff --git a/src/store.ts b/src/store.ts
index ddb35cd2d9cc2e87e3855671fbf4305330cb809b..69812a8dd3bd2402a4c60b3c14157912148d3552 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -160,23 +160,30 @@ export class DiscordStore {
         }
     }
 
-    public async deleteUserToken(discordId: string): Promise<void> {
+    public async deleteUserToken(mxid: string): Promise<void> {
+        const res = await this.db.Get("SELECT * from user_id_discord_id WHERE user_id = $id", {
+            id: mxid,
+        });
+        if (!res) {
+            return;
+        }
+        const discordId = res.discord_id;
         log.silly("SQL", "deleteUserToken => ", discordId);
         try {
             await Promise.all([
                 this.db.Run(
                     `
-                    DELETE FROM user_id_discord_id WHERE discord_id = $id;
+                    DELETE FROM user_id_discord_id WHERE discord_id = $id
                     `
                 , {
-                    $id: discordId,
+                    id: discordId,
                 }),
                 this.db.Run(
                     `
-                    DELETE FROM discord_id_token WHERE discord_id = $id;
+                    DELETE FROM discord_id_token WHERE discord_id = $id
                     `
                 , {
-                    $id: discordId,
+                    id: discordId,
                 }),
             ]);
         } catch (err) {
diff --git a/src/structures/lock.ts b/src/structures/lock.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f400aab092a3a6f49381d9c47b43112d0500be9d
--- /dev/null
+++ b/src/structures/lock.ts
@@ -0,0 +1,60 @@
+export class Lock<T> {
+    private locks: Map<T, {i: NodeJS.Timeout|null, r: (() => void)|null}>;
+    private lockPromises: Map<T, Promise<{}>>;
+    constructor(
+        private timeout: number,
+    ) {
+        this.locks = new Map();
+        this.lockPromises = new Map();
+    }
+
+    public set(key: T) {
+        // if there is a lock set.....we don't set a second one ontop
+        if (this.locks.has(key)) {
+            return;
+        }
+
+        // set a dummy lock so that if we re-set again before releasing it won't do anthing
+        this.locks.set(key, {i: null, r: null});
+
+        const p = new Promise<{}>((resolve) => {
+            // first we check if the lock has the key....if not, e.g. if it
+            // got released too quickly, we still want to resolve our promise
+            if (!this.locks.has(key)) {
+                resolve();
+                return;
+            }
+            // create the interval that will release our promise after the timeout
+            const i = setTimeout(() => {
+                this.release(key);
+            }, this.timeout);
+            // aaand store to our lock
+            this.locks.set(key, {r: resolve, i});
+        });
+        this.lockPromises.set(key, p);
+    }
+
+    public release(key: T) {
+        // if there is nothing to release then there is nothing to release
+        if (!this.locks.has(key)) {
+            return;
+        }
+        const lock = this.locks.get(key)!;
+        if (lock.r !== null) {
+            lock.r();
+        }
+        if (lock.i !== null) {
+            clearTimeout(lock.i);
+        }
+        this.locks.delete(key);
+        this.lockPromises.delete(key);
+    }
+
+    public async wait(key: T) {
+        // we wait for a lock release only if a promise is present
+        const promise = this.lockPromises.get(key);
+        if (promise) {
+            await promise;
+        }
+    }
+}
diff --git a/test/structures/test_lock.ts b/test/structures/test_lock.ts
new file mode 100644
index 0000000000000000000000000000000000000000..114ddb9065f0f11a9acb288b43f71c58227ba827
--- /dev/null
+++ b/test/structures/test_lock.ts
@@ -0,0 +1,45 @@
+/*
+Copyright 2019 matrix-appservice-discord
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { expect } from "chai";
+import { Lock } from "../../src/structures/lock";
+import { Util } from "../../src/util";
+
+const LOCKTIMEOUT = 300;
+
+describe("Lock", () => {
+    it("should lock and unlock", async () => {
+        const lock = new Lock<string>(LOCKTIMEOUT);
+        const t = Date.now();
+        lock.set("bunny");
+        await lock.wait("bunny");
+        const diff = Date.now() - t;
+        expect(diff).to.be.greaterThan(LOCKTIMEOUT - 1);
+    });
+    it("should lock and unlock early, if unlocked", async () => {
+        const SHORTDELAY = 100;
+        const DELAY_ACCURACY = 5;
+        const lock = new Lock<string>(LOCKTIMEOUT);
+        setTimeout(() => lock.release("fox"), SHORTDELAY);
+        const t = Date.now();
+        lock.set("fox");
+        await lock.wait("fox");
+        const diff = Date.now() - t;
+        // accuracy can be off by a few ms soemtimes
+        expect(diff).to.be.greaterThan(SHORTDELAY - DELAY_ACCURACY);
+        expect(diff).to.be.lessThan(SHORTDELAY + DELAY_ACCURACY);
+    });
+});
diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts
index 10df5fd801e4b366a2d6b818e33787af382daa7b..a3e80852d899dfc207da66a9a1678e9b6b21e8ba 100644
--- a/test/test_discordbot.ts
+++ b/test/test_discordbot.ts
@@ -403,46 +403,4 @@ describe("DiscordBot", () => {
             assert.equal(expected, ITERATIONS);
         });
     });
-    describe("locks", () => {
-        it("should lock and unlock a channel", async () => {
-            const bot = new modDiscordBot.DiscordBot(
-                "",
-                config,
-                mockBridge,
-                {},
-            ) as DiscordBot;
-            const chan = new MockChannel("123") as any;
-            const t = Date.now();
-            bot.lockChannel(chan);
-            await bot.waitUnlock(chan);
-            const diff = Date.now() - t;
-            expect(diff).to.be.greaterThan(config.limits.discordSendDelay - 1);
-        });
-        it("should lock and unlock a channel early, if unlocked", async () => {
-            const discordSendDelay = 500;
-            const SHORTDELAY = 100;
-            const MINEXPECTEDDELAY = 95;
-            const bot = new modDiscordBot.DiscordBot(
-                "",
-                {
-                    bridge: {
-                        domain: "localhost",
-                    },
-                    limits: {
-                        discordSendDelay,
-                    },
-                },
-                mockBridge,
-                {},
-            ) as DiscordBot;
-            const chan = new MockChannel("123") as any;
-            setTimeout(() => bot.unlockChannel(chan), SHORTDELAY);
-            const t = Date.now();
-            bot.lockChannel(chan);
-            await bot.waitUnlock(chan);
-            const diff = Date.now() - t;
-            // Date accuracy can be off by a few ms sometimes.
-            expect(diff).to.be.greaterThan(MINEXPECTEDDELAY);
-        });
-    });
 });
diff --git a/test/test_discordmessageprocessor.ts b/test/test_discordmessageprocessor.ts
index d66dd949e2cbe76da2780a1853e5acd6775ee4b6..2938472011f76dedcae9df1de1a2c32f3962289e 100644
--- a/test/test_discordmessageprocessor.ts
+++ b/test/test_discordmessageprocessor.ts
@@ -391,6 +391,18 @@ describe("DiscordMessageProcessor", () => {
             Chai.assert.equal(reply, "<span data-mx-color=\"#dead88\"><strong>@role</strong></span>");
         });
     });
+    describe("InsertSpoiler / HTML", () => {
+        it("parses spoilers", () => {
+            const processor = new DiscordMessageProcessor(
+                new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot);
+            const content = { content: "foxies" };
+            let reply = processor.InsertSpoiler(content);
+            Chai.assert.equal(reply, "(Spoiler: foxies)");
+
+            reply = processor.InsertSpoiler(content, true);
+            Chai.assert.equal(reply, "<span data-mx-spoiler>foxies</span>");
+        });
+    });
     describe("InsertEmoji", () => {
         it("inserts static emojis to their post-parse flag", () => {
             const processor = new DiscordMessageProcessor(
diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts
index 03ba47112152cd1d654e5b66e2d071893d10d81f..91f350af5394cf007ff60cee86f9ad470ea32a5a 100644
--- a/test/test_matrixeventprocessor.ts
+++ b/test/test_matrixeventprocessor.ts
@@ -597,6 +597,22 @@ describe("MatrixEventProcessor", () => {
             } as IMatrixEvent, mxClient);
             expect(ret).equals("[filename.webm](https://localhost/8000000)");
         });
+        it("Should reply embeds on large info.size images if set", async () => {
+            const LARGE_FILE = 8000000;
+            const processor = createMatrixEventProcessor();
+            const ret = await processor.HandleAttachment({
+                content: {
+                    body: "filename.jpg",
+                    info: {
+                        mimetype: "image/jpeg",
+                        size: LARGE_FILE,
+                    },
+                    msgtype: "m.image",
+                    url: "mxc://localhost/8000000",
+                },
+            } as IMatrixEvent, mxClient, true);
+            expect((ret as Discord.RichEmbed).image!.url).equals("https://localhost/8000000");
+        });
         it("Should handle stickers.", async () => {
             const processor = createMatrixEventProcessor();
             const attachment = (await processor.HandleAttachment({
diff --git a/test/test_matrixmessageprocessor.ts b/test/test_matrixmessageprocessor.ts
index 3319248d0c5df278b9e81dcde44346c365a2e23a..d49c14bfe46e450331d0ff5793ad2e22bc682ea5 100644
--- a/test/test_matrixmessageprocessor.ts
+++ b/test/test_matrixmessageprocessor.ts
@@ -192,6 +192,13 @@ code
 **##### tail**
 **###### foxies**`);
         });
+        it("strips simple span tags", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<span>bunny</span>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("bunny");
+        });
     });
     describe("FormatMessage / formatted_body / complex", () => {
         it("html unescapes stuff inside of code", async () => {
@@ -340,6 +347,20 @@ code
             const result = await mp.FormatMessage(msg, guild as any);
             expect(result).is.equal("[*yay?*](http://example.com)");
         });
+        it("Handles spoilers", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<span data-mx-spoiler>foxies</span>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("||foxies||");
+        });
+        it("Handles spoilers with reason", async () => {
+            const mp = new MatrixMessageProcessor(bot);
+            const guild = new MockGuild("1234");
+            const msg = getHtmlMessage("<span data-mx-spoiler=\"floof\">foxies</span>");
+            const result = await mp.FormatMessage(msg, guild as any);
+            expect(result).is.equal("(floof)||foxies||");
+        });
     });
     describe("FormatMessage / formatted_body / emoji", () => {
         it("Inserts emoji by name", async () => {
diff --git a/tools/addRoomsToDirectory.ts b/tools/addRoomsToDirectory.ts
index 5b2f1840b0290a7c4c4e01ef7453e44a73077482..11c8a62b15e7b975eced6d57a5a1d907d7dc5473 100644
--- a/tools/addRoomsToDirectory.ts
+++ b/tools/addRoomsToDirectory.ts
@@ -42,10 +42,11 @@ const optionDefinitions = [
     },
     {
         alias: "r",
-        defaultValue: "room-store.db",
-        description: "The location of the registration file.",
-        name: "reg",
+        defaultValue: "discord-registration.yaml",
+        description: "The AS registration file.",
+        name: "registration",
         type: String,
+        typeLabel: "<discord-registration.yaml>",
     },
 ];
 
@@ -66,7 +67,7 @@ if (options.help) {
     process.exit(0);
 }
 
-const {store, appservice} = ToolsHelper.getToolDependencies(options.config, options.reg);
+const {store, appservice} = ToolsHelper.getToolDependencies(options.config, options.registration);
 
 async function run() {
     try {
diff --git a/tools/addbot.ts b/tools/addbot.ts
index b8ef769b7ad9c5e5e9fe4e372f370531290d81cd..2dda777537df95d53e6cbb8bdaed47175e89b2f6 100644
--- a/tools/addbot.ts
+++ b/tools/addbot.ts
@@ -20,9 +20,45 @@ limitations under the License.
  */
 import * as yaml from "js-yaml";
 import * as fs from "fs";
+import * as args from "command-line-args";
+import * as usage from "command-line-usage";
 import { Util } from "../src/util";
 
-const yamlConfig = yaml.safeLoad(fs.readFileSync("config.yaml", "utf8"));
+const optionDefinitions = [
+  {
+      alias: "h",
+      description: "Display this usage guide.",
+      name: "help",
+      type: Boolean,
+  },
+  {
+      alias: "c",
+      defaultValue: "config.yaml",
+      description: "The AS config file.",
+      name: "config",
+      type: String,
+      typeLabel: "<config.yaml>",
+  },
+];
+
+const options = args(optionDefinitions);
+
+if (options.help) {
+  /* tslint:disable:no-console */
+  console.log(usage([
+  {
+      content: "A tool to obtain the Discord bot invitation URL.",
+      header: "Add bot",
+  },
+  {
+      header: "Options",
+      optionList: optionDefinitions,
+  },
+  ]));
+  process.exit(0);
+}
+
+const yamlConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8"));
 if (yamlConfig === null) {
   console.error("You have an error in your discord config.");
 }
diff --git a/tools/adminme.ts b/tools/adminme.ts
index eaf4f0d9a4cb658373774d72e0bea97fb7414fbc..f8b28180a87a01e7330e0cfec1b5044556400060 100644
--- a/tools/adminme.ts
+++ b/tools/adminme.ts
@@ -40,6 +40,14 @@ const optionDefinitions = [
     },
     {
         alias: "r",
+        defaultValue: "discord-registration.yaml",
+        description: "The AS registration file.",
+        name: "registration",
+        type: String,
+        typeLabel: "<discord-registration.yaml>",
+    },
+    {
+        alias: "m",
         description: "The roomid to modify",
         name: "roomid",
         type: String,
diff --git a/tools/chanfix.ts b/tools/chanfix.ts
index a4fa45f79aa3387f6baf166191ab35a9efef1085..3e86a22ae5df4138be3de7ded4b348edc6fee313 100644
--- a/tools/chanfix.ts
+++ b/tools/chanfix.ts
@@ -38,6 +38,14 @@ const optionDefinitions = [
         type: String,
         typeLabel: "<config.yaml>",
     },
+    {
+        alias: "r",
+        defaultValue: "discord-registration.yaml",
+        description: "The AS registration file.",
+        name: "registration",
+        type: String,
+        typeLabel: "<discord-registration.yaml>",
+    },
 ];
 
 const options = args(optionDefinitions);
diff --git a/tools/ghostfix.ts b/tools/ghostfix.ts
index f8e9d6c2e9825b41c8b0690bb16e57686ef69e39..1d0310b2369ed2d2374326f861321421ba010d0f 100644
--- a/tools/ghostfix.ts
+++ b/tools/ghostfix.ts
@@ -49,6 +49,14 @@ const optionDefinitions = [
         type: String,
         typeLabel: "<config.yaml>",
     },
+    {
+        alias: "r",
+        defaultValue: "discord-registration.yaml",
+        description: "The AS registration file.",
+        name: "registration",
+        type: String,
+        typeLabel: "<discord-registration.yaml>",
+    },
 ];
 
 const options = args(optionDefinitions);