diff --git a/Dockerfile b/Dockerfile
index 0aa0e3d56db4bb73ddf843bef55eb838f8fb8f5f..83cecfca0628a4313caf1025f2ff1dc88da3b2be 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 FROM node:alpine AS BUILD
 COPY . /tmp/src
 # install some dependencies needed for the build process
-RUN apk add --no-cache -t build-deps make gcc g++ python ca-certificates libc-dev wget
+RUN apk add --no-cache -t build-deps make gcc g++ python ca-certificates libc-dev wget git
 RUN cd /tmp/src \
     && npm install \
     && npm run build
diff --git a/README.md b/README.md
index c1ce1e7afbf55a5c276424fd3423eeafd6b3d411..b98ee36ba88226e0fe7fba2eb60b803ecd1afc39 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ bridging, with one or two bugs cropping up.
 
 
 [![Build Status](https://travis-ci.org/Half-Shot/matrix-appservice-discord.svg?branch=develop)](https://travis-ci.org/Half-Shot/matrix-appservice-discord)
+[![Docker Automated build](https://img.shields.io/docker/builds/halfshot/matrix-appservice-discord.svg)](https://hub.docker.com/r/halfshot/matrix-appservice-discord)
 [![#discord:half-shot.uk](https://img.shields.io/matrix/discord:half-shot.uk.svg?server_fqdn=matrix.half-shot.uk&label=%23discord:half-shot.uk&logo=matrix)](https://matrix.to/#/#discord:half-shot.uk)
 
 ### PRs
@@ -30,7 +31,7 @@ The bridge supports any version of Node.js >= v10.X, including all [current rele
 
 ### Setup the bridge
 
-* Run ``npm install`` to grab the dependencies.
+* Run ``npm install`` to grab the dependencies. `npm` may complain about peer dependencies, but you can safely ignore these.
 * Run ``npm run build`` to build the typescript into javascript.
 * Copy ``config/config.sample.yaml`` to ``config.yaml`` and edit it to reflect your setup.
   * Note that you are expected to set ``domain`` and ``homeserverURL`` to your **public** host name.
@@ -79,6 +80,10 @@ docker build -t halfshot/matrix-appservice-discord .
 # Run the container
 docker run -v /matrix-appservice-discord:/data -p 9005:9005 halfshot/matrix-appservice-discord
 ```
+#### Metrics
+
+The bridge supports reporting metrics via Prometheus. You can configure metrics support in the config
+file. The metrics will be reported under the URL provided in the registration file, on the `/metrics` endpoint.
 
 #### 3PID Protocol Support
 
@@ -104,6 +109,7 @@ should show up in the network list on Riot and other clients.
 
 * For the bot to appear online on Discord you need to run the bridge itself.
 * ``npm start``
+* Particular configuration keys can be overridden by defining corresponding environment variables. For instance, `auth.botToken` can be set with `APPSERVICE_DISCORD_AUTH_BOT_TOKEN`.
 
 [Howto](./docs/howto.md)
 
diff --git a/package-lock.json b/package-lock.json
index 1d2bb4100a1651b5ff6c744b0217d9a08d731376..d245fce9e7be738eb720cc3f69e9310aed90910d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-appservice-discord",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -8,7 +8,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
       "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
-      "dev": true,
       "requires": {
         "@babel/highlight": "^7.0.0"
       }
@@ -67,7 +66,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
       "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
-      "dev": true,
       "requires": {
         "chalk": "^2.0.0",
         "esutils": "^2.0.2",
@@ -77,8 +75,7 @@
         "js-tokens": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-          "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-          "dev": true
+          "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
         }
       }
     },
@@ -190,11 +187,6 @@
       "integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==",
       "dev": true
     },
-    "@types/p-queue": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-3.0.0.tgz",
-      "integrity": "sha512-brMFTEZiltJnAxNy07LzRVw2bdQX1ElCElHEmo5WEI70oWQe00aXew9RKmpf8MOzBMiMfNeuQoEyQCWKawPzmQ=="
-    },
     "@types/sqlite3": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.3.tgz",
@@ -466,6 +458,33 @@
         "tar": "^4.4.6"
       }
     },
+    "bintrees": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
+      "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
+    },
+    "bluebird": {
+      "version": "3.5.5",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz",
+      "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w=="
+    },
+    "body-parser": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+      "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+      "requires": {
+        "bytes": "3.1.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.7.0",
+        "raw-body": "2.4.0",
+        "type-is": "~1.6.17"
+      }
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -497,8 +516,6 @@
       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
       "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
     },
-<<<<<<< HEAD
-=======
     "bytes": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -516,7 +533,6 @@
         "write-file-atomic": "^2.4.2"
       }
     },
->>>>>>> develop
     "caller-path": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
@@ -698,9 +714,9 @@
       }
     },
     "combined-stream": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
-      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
       "requires": {
         "delayed-stream": "~1.0.0"
       }
@@ -755,9 +771,12 @@
       }
     },
     "content-disposition": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
-      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+      "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+      "requires": {
+        "safe-buffer": "5.1.2"
+      }
     },
     "content-type": {
       "version": "1.0.4",
@@ -774,9 +793,9 @@
       }
     },
     "cookie": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
-      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+      "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
     },
     "cookie-signature": {
       "version": "1.0.6",
@@ -947,15 +966,15 @@
       }
     },
     "discord.js": {
-      "version": "11.4.2",
-      "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.4.2.tgz",
-      "integrity": "sha512-MDwpu0lMFTjqomijDl1Ed9miMQe6kB4ifKdP28QZllmLv/HVOJXhatRgjS8urp/wBlOfx+qAYSXcdI5cKGYsfg==",
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.5.1.tgz",
+      "integrity": "sha512-tGhV5xaZXE3Z+4uXJb3hYM6gQ1NmnSxp9PClcsSAYFVRzH6AJH74040mO3afPDMWEAlj8XsoPXXTJHTxesqcGw==",
       "requires": {
         "long": "^4.0.0",
         "prism-media": "^0.0.3",
         "snekfetch": "^3.6.4",
         "tweetnacl": "^1.0.0",
-        "ws": "^4.0.0"
+        "ws": "^6.0.0"
       }
     },
     "doctrine": {
@@ -1031,12 +1050,6 @@
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
       "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
     },
-<<<<<<< HEAD
-    "entities": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
-      "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
-=======
     "end-of-stream": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -1045,7 +1058,11 @@
       "requires": {
         "once": "^1.4.0"
       }
->>>>>>> develop
+    },
+    "entities": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+      "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
     },
     "env-variable": {
       "version": "0.0.5",
@@ -1361,109 +1378,40 @@
       "dev": true
     },
     "express": {
-      "version": "4.16.4",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
-      "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+      "version": "4.17.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+      "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
       "requires": {
-        "accepts": "~1.3.5",
+        "accepts": "~1.3.7",
         "array-flatten": "1.1.1",
-        "body-parser": "1.18.3",
-        "content-disposition": "0.5.2",
+        "body-parser": "1.19.0",
+        "content-disposition": "0.5.3",
         "content-type": "~1.0.4",
-        "cookie": "0.3.1",
+        "cookie": "0.4.0",
         "cookie-signature": "1.0.6",
         "debug": "2.6.9",
         "depd": "~1.1.2",
         "encodeurl": "~1.0.2",
         "escape-html": "~1.0.3",
         "etag": "~1.8.1",
-        "finalhandler": "1.1.1",
+        "finalhandler": "~1.1.2",
         "fresh": "0.5.2",
         "merge-descriptors": "1.0.1",
         "methods": "~1.1.2",
         "on-finished": "~2.3.0",
-        "parseurl": "~1.3.2",
+        "parseurl": "~1.3.3",
         "path-to-regexp": "0.1.7",
-        "proxy-addr": "~2.0.4",
-        "qs": "6.5.2",
-        "range-parser": "~1.2.0",
+        "proxy-addr": "~2.0.5",
+        "qs": "6.7.0",
+        "range-parser": "~1.2.1",
         "safe-buffer": "5.1.2",
-        "send": "0.16.2",
-        "serve-static": "1.13.2",
-        "setprototypeof": "1.1.0",
-        "statuses": "~1.4.0",
-        "type-is": "~1.6.16",
+        "send": "0.17.1",
+        "serve-static": "1.14.1",
+        "setprototypeof": "1.1.1",
+        "statuses": "~1.5.0",
+        "type-is": "~1.6.18",
         "utils-merge": "1.0.1",
         "vary": "~1.1.2"
-      },
-      "dependencies": {
-        "body-parser": {
-          "version": "1.18.3",
-          "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
-          "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
-          "requires": {
-            "bytes": "3.0.0",
-            "content-type": "~1.0.4",
-            "debug": "2.6.9",
-            "depd": "~1.1.2",
-            "http-errors": "~1.6.3",
-            "iconv-lite": "0.4.23",
-            "on-finished": "~2.3.0",
-            "qs": "6.5.2",
-            "raw-body": "2.3.3",
-            "type-is": "~1.6.16"
-          }
-        },
-        "bytes": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
-          "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
-        },
-        "http-errors": {
-          "version": "1.6.3",
-          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
-          "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
-          "requires": {
-            "depd": "~1.1.2",
-            "inherits": "2.0.3",
-            "setprototypeof": "1.1.0",
-            "statuses": ">= 1.4.0 < 2"
-          }
-        },
-        "iconv-lite": {
-          "version": "0.4.23",
-          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
-          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
-          "requires": {
-            "safer-buffer": ">= 2.1.2 < 3"
-          }
-        },
-        "qs": {
-          "version": "6.5.2",
-          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
-        },
-        "raw-body": {
-          "version": "2.3.3",
-          "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
-          "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
-          "requires": {
-            "bytes": "3.0.0",
-            "http-errors": "1.6.3",
-            "iconv-lite": "0.4.23",
-            "unpipe": "1.0.0"
-          }
-        },
-        "setprototypeof": {
-          "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
-          "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
-        },
-        "statuses": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
-          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
-        }
       }
     },
     "extend": {
@@ -1541,24 +1489,17 @@
       }
     },
     "finalhandler": {
-      "version": "1.1.1",
-      "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
-      "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+      "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
       "requires": {
         "debug": "2.6.9",
         "encodeurl": "~1.0.2",
         "escape-html": "~1.0.3",
         "on-finished": "~2.3.0",
-        "parseurl": "~1.3.2",
-        "statuses": "~1.4.0",
+        "parseurl": "~1.3.3",
+        "statuses": "~1.5.0",
         "unpipe": "~1.0.0"
-      },
-      "dependencies": {
-        "statuses": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
-          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
-        }
       }
     },
     "find-cache-dir": {
@@ -1792,7 +1733,6 @@
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
     },
-<<<<<<< HEAD
     "hash.js": {
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
@@ -1800,7 +1740,8 @@
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
-=======
+      }
+    },
     "hasha": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz",
@@ -1808,7 +1749,6 @@
       "dev": true,
       "requires": {
         "is-stream": "^1.0.1"
->>>>>>> develop
       }
     },
     "he": {
@@ -1821,7 +1761,17 @@
       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz",
       "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A=="
     },
-<<<<<<< HEAD
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+      "dev": true
+    },
+    "htmlencode": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz",
+      "integrity": "sha1-9+LWr74YqHp45jujMI51N2Z0Dj8="
+    },
     "htmlparser2": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -1836,21 +1786,16 @@
       },
       "dependencies": {
         "readable-stream": {
-          "version": "3.3.0",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
-          "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==",
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
+          "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
           "requires": {
             "inherits": "^2.0.3",
             "string_decoder": "^1.1.1",
             "util-deprecate": "^1.0.1"
           }
         }
-=======
-    "hosted-git-info": {
-      "version": "2.7.1",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
-      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
-      "dev": true
+      }
     },
     "http-errors": {
       "version": "1.7.2",
@@ -1862,7 +1807,6 @@
         "setprototypeof": "1.1.1",
         "statuses": ">= 1.5.0 < 2",
         "toidentifier": "1.0.0"
->>>>>>> develop
       }
     },
     "http-signature": {
@@ -1875,6 +1819,14 @@
         "sshpk": "^1.7.0"
       }
     },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
     "ignore": {
       "version": "3.3.10",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
@@ -2372,16 +2324,6 @@
         "type-check": "~0.3.2"
       }
     },
-<<<<<<< HEAD
-=======
-    "lie": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
-      "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
-      "requires": {
-        "immediate": "~3.0.5"
-      }
-    },
     "load-json-file": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
@@ -2402,14 +2344,6 @@
         }
       }
     },
-    "localforage": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
-      "integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
-      "requires": {
-        "lie": "3.1.1"
-      }
-    },
     "locate-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
@@ -2420,13 +2354,11 @@
         "path-exists": "^3.0.0"
       }
     },
->>>>>>> develop
     "lodash": {
       "version": "4.17.11",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
       "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
     },
-<<<<<<< HEAD
     "lodash.clonedeep": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
@@ -2437,6 +2369,12 @@
       "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
       "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c="
     },
+    "lodash.flattendeep": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
+      "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
+      "dev": true
+    },
     "lodash.isplainobject": {
       "version": "4.0.6",
       "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -2451,13 +2389,6 @@
       "version": "4.6.1",
       "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
       "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ=="
-=======
-    "lodash.flattendeep": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
-      "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
-      "dev": true
->>>>>>> develop
     },
     "lodash.padend": {
       "version": "4.6.1",
@@ -2488,7 +2419,6 @@
       "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
       "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
     },
-<<<<<<< HEAD
     "lowdb": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz",
@@ -2508,7 +2438,6 @@
         }
       }
     },
-=======
     "lru-cache": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
@@ -2551,15 +2480,11 @@
       "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
       "dev": true
     },
->>>>>>> develop
     "manakin": {
       "version": "0.5.2",
       "resolved": "https://registry.npmjs.org/manakin/-/manakin-0.5.2.tgz",
       "integrity": "sha512-pfDSB7QYoVg0Io4KMV9hhPoXpj6p0uBscgtyUSKCOFZe8bqgbpStfgnKIbF/ulnr6U3ICu4OqdyxAqBgOhZwBQ=="
     },
-<<<<<<< HEAD
-    "matrix-bot-sdk": {
-=======
     "map-age-cleaner": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
@@ -2569,73 +2494,53 @@
         "p-defer": "^1.0.0"
       }
     },
-    "matrix-appservice": {
->>>>>>> develop
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/matrix-bot-sdk/-/matrix-bot-sdk-0.3.5.tgz",
-      "integrity": "sha512-/FnQ+bbZcTfAHGBPVX9tBezLR3D7omprmLYCyf2XvGkXaUqUbW/JKfILGEyIbzSPPrWcqm+vn8VGGGBldevDRQ==",
+    "matrix-bot-sdk": {
+      "version": "github:turt2live/matrix-js-bot-sdk#0c3d9bcde0157a638fa4c3573d6201e4d56d2a1e",
+      "from": "github:turt2live/matrix-js-bot-sdk#develop",
       "requires": {
-        "@types/node": "^8.10.34",
-        "bluebird": "^3.5.2",
-        "express": "^4.16.4",
+        "@types/node": "^10.14.9",
+        "bluebird": "^3.5.5",
+        "express": "^4.17.1",
         "hash.js": "^1.1.7",
+        "htmlencode": "0.0.4",
         "lowdb": "^1.0.0",
         "morgan": "^1.9.1",
-<<<<<<< HEAD
-=======
-        "request": "^2.88.0"
-      }
-    },
-    "matrix-appservice-bridge": {
-      "version": "github:matrix-org/matrix-appservice-bridge#8a7288edf1d1d1d1395a83d330d836d9c9bf1e76",
-      "from": "github:matrix-org/matrix-appservice-bridge#8a7288edf1d1d1d1395a83d330d836d9c9bf1e76",
-      "requires": {
-        "bluebird": "^2.9.34",
-        "chalk": "^2.4.1",
-        "extend": "^3.0.0",
-        "is-my-json-valid": "^2.19.0",
-        "js-yaml": "^3.12.0",
-        "matrix-appservice": "0.3.5",
-        "matrix-js-sdk": "^1.0.1",
-        "nedb": "^1.1.3",
-        "nopt": "^3.0.3",
-        "prom-client": "^11.1.1",
-        "request": "^2.61.0",
-        "winston": "^3.1.0",
-        "winston-daily-rotate-file": "^3.3.3"
-      }
-    },
-    "matrix-js-sdk": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-1.1.0.tgz",
-      "integrity": "sha512-ECoMN6DkwPdKiMa/jSoMkSDngFCo6x7oH84rLd1NtD7lBPl3Ejj6ARa0iIELE7u0OUO6J0FzdWh7Hd0ZnVTmww==",
-      "requires": {
-        "another-json": "^0.2.0",
-        "babel-runtime": "^6.26.0",
-        "base-x": "3.0.4",
-        "bluebird": "^3.5.0",
-        "browser-request": "^0.3.3",
-        "bs58": "^4.0.1",
-        "content-type": "^1.0.2",
-        "loglevel": "1.6.1",
-        "qs": "^6.5.2",
->>>>>>> develop
         "request": "^2.88.0",
-        "request-promise": "^4.2.2",
-        "sanitize-html": "^1.20.0",
-        "tslint": "^5.11.0",
-        "typescript": "^3.1.1"
+        "request-promise": "^4.2.4",
+        "sanitize-html": "^1.20.1",
+        "tslint": "^5.17.0",
+        "typescript": "^3.5.2"
       },
       "dependencies": {
         "@types/node": {
-          "version": "8.10.48",
-          "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.48.tgz",
-          "integrity": "sha512-c35YEBTkL4rzXY2ucpSKy+UYHjUBIIkuJbWYbsGIrKLEWU5dgJMmLkkIb3qeC3O3Tpb1ZQCwecscvJTDjDjkRw=="
+          "version": "10.14.9",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.9.tgz",
+          "integrity": "sha512-NelG/dSahlXYtSoVPErrp06tYFrvzj8XLWmKA+X8x0W//4MqbUyZu++giUG/v0bjAT6/Qxa8IjodrfdACyb0Fg=="
         },
-        "bluebird": {
-          "version": "3.5.4",
-          "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
-          "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw=="
+        "tslint": {
+          "version": "5.17.0",
+          "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.17.0.tgz",
+          "integrity": "sha512-pflx87WfVoYepTet3xLfDOLDm9Jqi61UXIKePOuca0qoAZyrGWonDG9VTbji58Fy+8gciUn8Bt7y69+KEVjc/w==",
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "builtin-modules": "^1.1.1",
+            "chalk": "^2.3.0",
+            "commander": "^2.12.1",
+            "diff": "^3.2.0",
+            "glob": "^7.1.1",
+            "js-yaml": "^3.13.1",
+            "minimatch": "^3.0.4",
+            "mkdirp": "^0.5.1",
+            "resolve": "^1.3.2",
+            "semver": "^5.3.0",
+            "tslib": "^1.8.0",
+            "tsutils": "^2.29.0"
+          }
+        },
+        "typescript": {
+          "version": "3.5.2",
+          "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz",
+          "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA=="
         }
       }
     },
@@ -2700,18 +2605,16 @@
         "mime-db": "1.40.0"
       }
     },
-<<<<<<< HEAD
-    "minimalistic-assert": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
-=======
     "mimic-fn": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
       "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
       "dev": true
->>>>>>> develop
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
     },
     "minimatch": {
       "version": "3.0.4",
@@ -3116,11 +3019,6 @@
         "p-limit": "^2.0.0"
       }
     },
-    "p-queue": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-3.0.0.tgz",
-      "integrity": "sha512-2tv/MRmPXfmfnjLLJAHl+DdU8p2DhZafAnlpm/C/T5BpF5L9wKz5tMin4A4N2zVpJL2YMhPlRmtO7s5EtNrjfA=="
-    },
     "p-try": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -3317,9 +3215,9 @@
       "dev": true
     },
     "postcss": {
-      "version": "7.0.15",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.15.tgz",
-      "integrity": "sha512-+avadY713SyQf0m5np7byFzAFZyhPhXyxwp8OVmdd5mKOxm0VzM2AJkKIgBro7gGVk4kYlCDvBVrSqhU5m8E+w==",
+      "version": "7.0.17",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz",
+      "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==",
       "requires": {
         "chalk": "^2.4.2",
         "source-map": "^0.6.1",
@@ -3406,6 +3304,14 @@
       "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
       "dev": true
     },
+    "prom-client": {
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.1.tgz",
+      "integrity": "sha512-AcFuxVgzoA/4nlpeg9SkM2HkDjNU3V7g2LCLwpudXSbcSLiFpRMVfsCoCY5RYeR/d9jkQng1mCmVKj1mPHvP0Q==",
+      "requires": {
+        "tdigest": "^0.1.1"
+      }
+    },
     "proxy-addr": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
@@ -3441,9 +3347,9 @@
       "dev": true
     },
     "psl": {
-      "version": "1.1.31",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
-      "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
+      "version": "1.1.32",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
+      "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g=="
     },
     "pump": {
       "version": "3.0.0",
@@ -3460,13 +3366,16 @@
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
     },
+    "qs": {
+      "version": "6.7.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+      "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+    },
     "range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
       "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
     },
-<<<<<<< HEAD
-=======
     "raw-body": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
@@ -3499,7 +3408,6 @@
         "read-pkg": "^3.0.0"
       }
     },
->>>>>>> develop
     "readable-stream": {
       "version": "2.3.6",
       "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
@@ -3539,13 +3447,6 @@
       "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz",
       "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc="
     },
-<<<<<<< HEAD
-=======
-    "regenerator-runtime": {
-      "version": "0.11.1",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
-      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
-    },
     "release-zalgo": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
@@ -3555,7 +3456,6 @@
         "es6-error": "^4.0.1"
       }
     },
->>>>>>> develop
     "request": {
       "version": "2.88.0",
       "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
@@ -3590,7 +3490,6 @@
         }
       }
     },
-<<<<<<< HEAD
     "request-promise": {
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz",
@@ -3600,13 +3499,6 @@
         "request-promise-core": "1.1.2",
         "stealthy-require": "^1.1.1",
         "tough-cookie": "^2.3.3"
-      },
-      "dependencies": {
-        "bluebird": {
-          "version": "3.5.4",
-          "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
-          "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw=="
-        }
       }
     },
     "request-promise-core": {
@@ -3616,7 +3508,7 @@
       "requires": {
         "lodash": "^4.17.11"
       }
-=======
+    },
     "require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -3628,7 +3520,6 @@
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
       "dev": true
->>>>>>> develop
     },
     "require-uncached": {
       "version": "1.0.3",
@@ -3721,9 +3612,9 @@
       "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
     },
     "send": {
-      "version": "0.16.2",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
-      "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+      "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
       "requires": {
         "debug": "2.6.9",
         "depd": "~1.1.2",
@@ -3732,55 +3623,32 @@
         "escape-html": "~1.0.3",
         "etag": "~1.8.1",
         "fresh": "0.5.2",
-        "http-errors": "~1.6.2",
-        "mime": "1.4.1",
-        "ms": "2.0.0",
+        "http-errors": "~1.7.2",
+        "mime": "1.6.0",
+        "ms": "2.1.1",
         "on-finished": "~2.3.0",
-        "range-parser": "~1.2.0",
-        "statuses": "~1.4.0"
+        "range-parser": "~1.2.1",
+        "statuses": "~1.5.0"
       },
       "dependencies": {
-        "http-errors": {
-          "version": "1.6.3",
-          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
-          "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
-          "requires": {
-            "depd": "~1.1.2",
-            "inherits": "2.0.3",
-            "setprototypeof": "1.1.0",
-            "statuses": ">= 1.4.0 < 2"
-          }
-        },
-        "mime": {
-          "version": "1.4.1",
-          "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
-          "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
-        },
-        "setprototypeof": {
-          "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
-          "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
-        },
-        "statuses": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
-          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
         }
       }
     },
     "serve-static": {
-      "version": "1.13.2",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
-      "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+      "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
       "requires": {
         "encodeurl": "~1.0.2",
         "escape-html": "~1.0.3",
-        "parseurl": "~1.3.2",
-        "send": "0.16.2"
+        "parseurl": "~1.3.3",
+        "send": "0.17.1"
       }
     },
-<<<<<<< HEAD
-=======
     "set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -3807,7 +3675,6 @@
       "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
       "dev": true
     },
->>>>>>> develop
     "shelljs": {
       "version": "0.7.8",
       "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz",
@@ -3984,6 +3851,11 @@
       "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=",
       "dev": true
     },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+    },
     "stealthy-require": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
@@ -4160,8 +4032,6 @@
         "yallist": "^3.0.2"
       }
     },
-<<<<<<< HEAD
-=======
     "tdigest": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz",
@@ -4182,7 +4052,6 @@
         "require-main-filename": "^2.0.0"
       }
     },
->>>>>>> develop
     "test-value": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz",
@@ -4218,8 +4087,6 @@
       "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
       "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
     },
-<<<<<<< HEAD
-=======
     "to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -4231,7 +4098,6 @@
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
       "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
     },
->>>>>>> develop
     "tough-cookie": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
@@ -4313,9 +4179,9 @@
       }
     },
     "tweetnacl": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz",
-      "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins="
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz",
+      "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A=="
     },
     "type-check": {
       "version": "0.3.2",
@@ -4614,12 +4480,11 @@
       }
     },
     "ws": {
-      "version": "4.1.0",
-      "resolved": "http://registry.npmjs.org/ws/-/ws-4.1.0.tgz",
-      "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
+      "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
       "requires": {
-        "async-limiter": "~1.0.0",
-        "safe-buffer": "~5.1.0"
+        "async-limiter": "~1.0.0"
       }
     },
     "xtend": {
diff --git a/package.json b/package.json
index 0dcafcb053dc8768043fc5356de5b2acd5a472a1..aff9171452cf8e804bc12a47472bcfeb7e8b14c3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-appservice-discord",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "description": "A bridge between Matrix and Discord",
   "main": "discordas.js",
   "scripts": {
@@ -36,21 +36,19 @@
   "dependencies": {
     "@types/command-line-args": "^5.0.0",
     "@types/js-yaml": "^3.12.1",
-    "@types/p-queue": "^3.0.0",
     "better-sqlite3": "^5.0.1",
     "command-line-args": "^4.0.7",
     "command-line-usage": "^4.1.0",
     "discord-markdown": "^2.0.0",
-    "discord.js": "^11.4.2",
+    "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.3.5",
+    "matrix-bot-sdk": "turt2live/matrix-js-bot-sdk#develop",
     "mime": "^1.6.0",
-    "moment": "^2.22.2",
     "node-html-parser": "^1.1.11",
-    "p-queue": "^3.0.0",
     "pg-promise": "^8.5.1",
+    "prom-client": "^11.3.0",
     "tslint": "^5.11.0",
     "typescript": "^3.1.3",
     "winston": "^3.0.0",
diff --git a/src/bot.ts b/src/bot.ts
index 22c180ae0b6febaf21cb0e357bbd86557dea4b58..23457ab9dc0c631b03a607da97f3712505020ab5 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -37,6 +37,7 @@ import * as mime from "mime";
 import { IMatrixEvent, IMatrixMediaInfo } from "./matrixtypes";
 import { Appservice, Intent } from "matrix-bot-sdk";
 import { DiscordCommandHandler } from "./discordcommandhandler";
+import { MetricPeg } from "./metrics";
 
 const log = new Log("DiscordBot");
 
@@ -81,8 +82,8 @@ export class DiscordBot {
 
     /* Handles messages queued up to be sent to matrix from discord. */
     private discordMessageQueue: { [channelId: string]: Promise<void> };
-    private channelLocks: { [channelId: string]: {p: Promise<{}>, i: NodeJS.Timeout|null} };
-
+    private channelLocks: Map<string, {i: NodeJS.Timeout|null, r: (() => void)|null}>;
+    private channelLockPromises: Map<string, Promise<{}>>;
     constructor(
         private config: DiscordBridgeConfig,
         private bridge: Appservice,
@@ -105,7 +106,8 @@ export class DiscordBot {
         // init vars
         this.sentMessages = [];
         this.discordMessageQueue = {};
-        this.channelLocks = {};
+        this.channelLocks = new Map();
+        this.channelLockPromises = new Map();
         this.lastEventIds = {};
     }
 
@@ -138,34 +140,39 @@ export class DiscordBot {
     }
 
     public lockChannel(channel: Discord.Channel) {
-        if (this.channelLocks[channel.id]) {
+        if (this.channelLocks.has(channel.id)) {
             return;
         }
 
-        const p = new Promise((resolve) => {
-            if (!this.channelLocks[channel.id]) {
-                resolve();
-                return;
-            }
-            const i = setInterval(resolve, this.config.limits.discordSendDelay);
-            this.channelLocks[channel.id].i = i;
+        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.channelLocks[channel.id] = {i: null, p};
+        this.channelLockPromises.set(channel.id, p);
     }
 
     public unlockChannel(channel: Discord.Channel) {
-        const lock = this.channelLocks[channel.id];
-        if (lock && lock.i !== null) {
+        if (!this.channelLocks.has(channel.id)) {
+            return;
+        }
+        const lock = this.channelLocks.get(channel.id)!;
+        if (lock.i !== null) {
+            lock.r!();
             clearTimeout(lock.i);
         }
-        delete this.channelLocks[channel.id];
+        this.channelLocks.delete(channel.id);
+        this.channelLockPromises.delete(channel.id);
     }
 
     public async waitUnlock(channel: Discord.Channel) {
-        const lock = this.channelLocks[channel.id];
-        if (lock) {
-            await lock.p;
+        const promise = this.channelLockPromises.get(channel.id);
+        if (promise) {
+            await promise;
         }
     }
 
@@ -233,6 +240,7 @@ export class DiscordBot {
         client.on("messageDelete", async (msg: Discord.Message) => {
             try {
                 await this.waitUnlock(msg.channel);
+                this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel);
                 this.discordMessageQueue[msg.channel.id] = (async () => {
                     await (this.discordMessageQueue[msg.channel.id] || Promise.resolve());
                     try {
@@ -253,6 +261,7 @@ export class DiscordBot {
                     promiseArr.push(async () => {
                         try {
                             await this.waitUnlock(msg.channel);
+                            this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel);
                             await this.DeleteDiscordMessage(msg);
                         } catch (err) {
                             log.error("Caught while handling 'messageDeleteBulk'", err);
@@ -267,6 +276,7 @@ export class DiscordBot {
         client.on("messageUpdate", async (oldMessage: Discord.Message, newMessage: Discord.Message) => {
             try {
                 await this.waitUnlock(newMessage.channel);
+                this.clientFactory.bindMetricsToChannel(newMessage.channel as Discord.TextChannel);
                 this.discordMessageQueue[newMessage.channel.id] = (async () => {
                     await (this.discordMessageQueue[newMessage.channel.id] || Promise.resolve());
                     try {
@@ -281,12 +291,15 @@ export class DiscordBot {
         });
         client.on("message", async (msg: Discord.Message) => {
             try {
+                MetricPeg.get.registerRequest(msg.id);
                 await this.waitUnlock(msg.channel);
+                this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel);
                 this.discordMessageQueue[msg.channel.id] = (async () => {
                     await (this.discordMessageQueue[msg.channel.id] || Promise.resolve());
                     try {
                         await this.OnMessage(msg);
                     } catch (err) {
+                        MetricPeg.get.requestOutcome(msg.id, true, "fail");
                         log.error("Caught while handing 'message'", err);
                     }
                 })();
@@ -382,6 +395,7 @@ export class DiscordBot {
             }
             const channel = guild.channels.get(room);
             if (channel && channel.type === "text") {
+                this.ClientFactory.bindMetricsToChannel(channel as Discord.TextChannel);
                 const lookupResult = new ChannelLookupResult();
                 lookupResult.channel = channel as Discord.TextChannel;
                 lookupResult.botUser = this.bot.user.id === client.user.id;
@@ -438,9 +452,10 @@ export class DiscordBot {
         try {
             this.lockChannel(chan);
             if (!botUser) {
-                opts.embed = embedSet.replyEmbed;
+                // NOTE: Don't send replies to discord if we are a puppet.
                 msg = await chan.send(embed.description, opts);
             } else if (hook) {
+                MetricPeg.get.remoteCall("hook.send");
                 msg = await hook.send(embed.description, {
                     avatarURL: embed!.author!.icon_url,
                     embeds: embedSet.replyEmbed ? [embedSet.replyEmbed] : undefined,
@@ -455,8 +470,13 @@ export class DiscordBot {
                 opts.embed = embed;
                 msg = await chan.send("", opts);
             }
-            await this.StoreMessagesSent(msg, chan, event);
-            this.unlockChannel(chan);
+            // Don't block on this.
+            this.StoreMessagesSent(msg, chan, event).then(() => {
+                this.unlockChannel(chan);
+            }).catch(() => {
+                log.warn("Failed to store sent message for ", event.event_id);
+            });
+
         } catch (err) {
             log.error("Couldn't send message. ", err);
         }
@@ -532,6 +552,7 @@ export class DiscordBot {
         if (guild) {
             const channel = client.channels.get(entry.remote!.get("discord_channel") as string);
             if (channel) {
+                this.ClientFactory.bindMetricsToChannel(channel as Discord.TextChannel);
                 return channel;
             }
             throw Error("Channel given in room entry not found");
@@ -733,12 +754,14 @@ export class DiscordBot {
         if (indexOfMsg !== -1) {
             log.verbose("Got repeated message, ignoring.");
             delete this.sentMessages[indexOfMsg];
+            MetricPeg.get.requestOutcome(msg.id, true, "dropped");
             return; // Skip *our* messages
         }
         const chan = msg.channel as Discord.TextChannel;
         if (msg.author.id === this.bot.user.id) {
             // We don't support double bridging.
             log.verbose("Not reflecting bot's own messages");
+            MetricPeg.get.requestOutcome(msg.id, true, "dropped");
             return;
         }
         // Test for webhooks
@@ -748,6 +771,8 @@ export class DiscordBot {
             if (webhook && msg.webhookID === webhook.id) {
                 // Filter out our own webhook messages.
                 log.verbose("Not reflecting own webhook messages");
+              // Filter out our own webhook messages.
+                MetricPeg.get.requestOutcome(msg.id, true, "dropped");
                 return;
             }
         }
@@ -755,6 +780,7 @@ export class DiscordBot {
         // check if it is a command to process by the bot itself
         if (msg.content.startsWith("!matrix")) {
             await this.discordCommandHandler.Process(msg);
+            MetricPeg.get.requestOutcome(msg.id, true, "success");
             return;
         }
 
@@ -763,14 +789,15 @@ export class DiscordBot {
         let rooms;
         try {
             rooms = await this.channelSync.GetRoomIdsFromChannel(msg.channel);
+            if (rooms === null) {
+                throw Error();
+            }
         } catch (err) {
             log.verbose("No bridged rooms to send message to. Oh well.");
+            MetricPeg.get.requestOutcome(msg.id, true, "dropped");
             return null;
         }
         try {
-            if (rooms === null) {
-              return null;
-            }
             const intent = this.GetIntentFromDiscordMember(msg.author, msg.webhookID);
             // Check Attachements
             await Util.AsyncForEach(msg.attachments.array(), async (attachment) => {
@@ -856,7 +883,9 @@ export class DiscordBot {
                     await afterSend(res);
                 }
             });
+            MetricPeg.get.requestOutcome(msg.id, true, "success");
         } catch (err) {
+            MetricPeg.get.requestOutcome(msg.id, true, "fail");
             log.verbose("Failed to send message into room.", err);
         }
     }
diff --git a/src/channelsyncroniser.ts b/src/channelsyncroniser.ts
index 1bb5817588e938bdc51bc6be9b0ce67597d7d224..ee1cb85bfbc66198690aecbefaf60944686a286b 100644
--- a/src/channelsyncroniser.ts
+++ b/src/channelsyncroniser.ts
@@ -210,7 +210,9 @@ export class ChannelSyncroniser {
         const icon = channel.guild.icon;
         let iconUrl: string | null = null;
         if (icon) {
-            iconUrl = `https://cdn.discordapp.com/icons/${channel.guild.id}/${icon}.png`;
+            // if discord prefixes their icon hashes with "a_" it means that they are animated
+            const animatedIcon = icon.startsWith("a_");
+            iconUrl = `https://cdn.discordapp.com/icons/${channel.guild.id}/${icon}.${animatedIcon ? "gif" : "png"}`;
         }
         remoteRooms.forEach((remoteRoom) => {
             const mxid = remoteRoom.matrix!.getId();
diff --git a/src/clientfactory.ts b/src/clientfactory.ts
index ae5f930c96221c627ee3d2413f8ce5e0c02574d9..1b2adcfb706231d74d0667c0612962b2208d55c6 100644
--- a/src/clientfactory.ts
+++ b/src/clientfactory.ts
@@ -16,8 +16,9 @@ limitations under the License.
 
 import { DiscordBridgeConfigAuth } from "./config";
 import { DiscordStore } from "./store";
-import { Client as DiscordClient } from "discord.js";
+import { Client as DiscordClient, TextChannel } from "discord.js";
 import { Log } from "./log";
+import { MetricPeg } from "./metrics";
 
 const log = new Log("ClientFactory");
 
@@ -107,4 +108,19 @@ export class DiscordClientFactory {
             return this.botClient;
         }
     }
+
+    public bindMetricsToChannel(channel: TextChannel) {
+        // tslint:disable-next-line:no-any
+        const flexChan = channel as any;
+        if (flexChan._xmet_send !== undefined) {
+            return;
+        }
+        // Prefix the real functions with _xmet_
+        flexChan._xmet_send = channel.send;
+        // tslint:disable-next-line:only-arrow-functions
+        channel.send = function() {
+            MetricPeg.get.remoteCall("channel.send");
+            return flexChan._xmet_send.apply(channel, arguments);
+        };
+    }
 }
diff --git a/src/config.ts b/src/config.ts
index 3c1ed9cc2ae83eb5dc711ec1c28e513c611dc08a..94c5a91fc34037fe663c86226b58ab61f13e5b80 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+const ENV_PREFIX = "APPSERVICE_DISCORD";
+const ENV_KEY_SEPARATOR = "_";
+const ENV_VAL_SEPARATOR = ",";
+
 /** Type annotations for config/config.schema.yaml */
 export class DiscordBridgeConfig {
     public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge();
@@ -27,18 +31,48 @@ export class DiscordBridgeConfig {
 
     /**
      * Apply a set of keys and values over the default config.
-     * @param _config Config keys
+     * @param newConfig Config keys
      * @param configLayer Private parameter
      */
     // tslint:disable-next-line no-any
-    public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) {
+    public applyConfig(newConfig: {[key: string]: any}, configLayer: {[key: string]: any} = this) {
           Object.keys(newConfig).forEach((key) => {
-            if ( typeof(configLayer[key]) === "object" &&
-                    !Array.isArray(configLayer[key])) {
-                this.ApplyConfig(newConfig[key], this[key]);
-                return;
+            if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) {
+                this.applyConfig(newConfig[key], configLayer[key]);
+            } else {
+                configLayer[key] = newConfig[key];
+            }
+        });
+    }
+
+    /**
+     * Override configuration keys defined in the supplied environment dictionary.
+     * @param environment environment variable dictionary
+     * @param path private parameter: config layer path determining the environment key prefix
+     * @param configLayer private parameter: current layer of configuration to alter recursively
+     */
+    public applyEnvironmentOverrides(
+        // tslint:disable-next-line no-any
+        environment: {[key: string]: any},
+        path: string[] = [ENV_PREFIX],
+        // tslint:disable-next-line no-any
+        configLayer: {[key: string]: any} = this,
+    ) {
+        Object.keys(configLayer).forEach((key) => {
+            // camelCase to THICK_SNAKE
+            const attributeKey = key.replace(/[A-Z]/g, (prefix) => `${ENV_KEY_SEPARATOR}${prefix}`).toUpperCase();
+            const attributePath = path.concat([attributeKey]);
+
+            if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) {
+                this.applyEnvironmentOverrides(environment, attributePath, configLayer[key]);
+            } else {
+                const lookupKey = attributePath.join(ENV_KEY_SEPARATOR);
+                if (lookupKey in environment) {
+                    configLayer[key] = (configLayer[key] instanceof Array)
+                        ? environment[lookupKey].split(ENV_VAL_SEPARATOR)
+                        : environment[lookupKey];
+                }
             }
-            configLayer[key] = newConfig[key];
         });
     }
 }
@@ -58,6 +92,7 @@ class DiscordBridgeConfigBridge {
     public disableEveryoneMention: boolean = false;
     public disableHereMention: boolean = false;
     public disableJoinLeaveNotifications: boolean = false;
+    public enableMetrics: boolean = false;
 }
 
 export class DiscordBridgeConfigDatabase {
diff --git a/src/db/roomstore.ts b/src/db/roomstore.ts
index c04230e0fa56c75046a628935f598454b7c6fc7c..ed311a8ca450258f1aeb83fd2d7fb33ba37cc293 100644
--- a/src/db/roomstore.ts
+++ b/src/db/roomstore.ts
@@ -18,7 +18,8 @@ import { IDatabaseConnector } from "./connector";
 import { Util } from "../util";
 
 import * as uuid from "uuid/v4";
-import { Postgres } from "./postgres";
+import { MetricPeg } from "../metrics";
+import { TimedCache } from "../structures/timedcache";
 
 const log = new Log("DbRoomStore");
 
@@ -92,9 +93,9 @@ const ENTRY_CACHE_LIMETIME = 30000;
 // XXX: This only implements functions used in the bridge at the moment.
 export class DbRoomStore {
 
-    private entriesMatrixIdCache: Map<string, {e: IRoomStoreEntry[], ts: number}>;
+    private entriesMatrixIdCache: TimedCache<string, IRoomStoreEntry[]>;
     constructor(private db: IDatabaseConnector) {
-        this.entriesMatrixIdCache = new Map();
+        this.entriesMatrixIdCache = new TimedCache(ENTRY_CACHE_LIMETIME);
     }
 
     public async upsertEntry(entry: IRoomStoreEntry) {
@@ -154,9 +155,11 @@ export class DbRoomStore {
 
     public async getEntriesByMatrixId(matrixId: string): Promise<IRoomStoreEntry[]> {
         const cached = this.entriesMatrixIdCache.get(matrixId);
-        if (cached && cached.ts + ENTRY_CACHE_LIMETIME > Date.now()) {
-            return cached.e;
+        if (cached) {
+            MetricPeg.get.storeCall("RoomStore.getEntriesByMatrixId", true);
+            return cached;
         }
+        MetricPeg.get.storeCall("RoomStore.getEntriesByMatrixId", false);
         const entries = await this.db.All(
             "SELECT * FROM room_entries WHERE matrix_id = $id", {id: matrixId},
         );
@@ -184,12 +187,13 @@ export class DbRoomStore {
             }
         }
         if (res.length > 0) {
-            this.entriesMatrixIdCache.set(matrixId, {e: res, ts: Date.now()});
+            this.entriesMatrixIdCache.set(matrixId, res);
         }
         return res;
     }
 
     public async getEntriesByMatrixIds(matrixIds: string[]): Promise<IRoomStoreEntry[]> {
+        MetricPeg.get.storeCall("RoomStore.getEntriesByMatrixIds", false);
         const mxIdMap = { };
         matrixIds.forEach((mxId, i) => mxIdMap[i] = mxId);
         const sql = `SELECT * FROM room_entries WHERE matrix_id IN (${matrixIds.map((_, id) => `\$${id}`).join(", ")})`;
@@ -222,6 +226,7 @@ export class DbRoomStore {
     }
 
     public async linkRooms(matrixRoom: MatrixStoreRoom, remoteRoom: RemoteStoreRoom) {
+        MetricPeg.get.storeCall("RoomStore.linkRooms", false);
         await this.upsertRoom(remoteRoom);
 
         const values = {
@@ -244,6 +249,7 @@ export class DbRoomStore {
     }
 
     public async getEntriesByRemoteRoomData(data: IRemoteRoomDataLazy): Promise<IRoomStoreEntry[]> {
+        MetricPeg.get.storeCall("RoomStore.getEntriesByRemoteRoomData", false);
         Object.keys(data).filter((k) => typeof(data[k]) === "boolean").forEach((k) => {
             data[k] = Number(data[k]);
         });
@@ -270,11 +276,13 @@ export class DbRoomStore {
     }
 
     public async removeEntriesByRemoteRoomId(remoteId: string) {
+        MetricPeg.get.storeCall("RoomStore.removeEntriesByRemoteRoomId", false);
         await this.db.Run(`DELETE FROM room_entries WHERE remote_id = $remoteId`, {remoteId});
         await this.db.Run(`DELETE FROM remote_room_data WHERE room_id = $remoteId`, {remoteId});
     }
 
     public async removeEntriesByMatrixRoomId(matrixId: string) {
+        MetricPeg.get.storeCall("RoomStore.removeEntriesByMatrixRoomId", false);
         const entries = (await this.db.All(`SELECT * FROM room_entries WHERE matrix_id = $matrixId`, {matrixId})) || [];
         await Util.AsyncForEach(entries, async (entry) => {
             if (entry.remote_id) {
@@ -286,6 +294,7 @@ export class DbRoomStore {
     }
 
     private async upsertRoom(room: RemoteStoreRoom) {
+        MetricPeg.get.storeCall("RoomStore.upsertRoom", false);
         if (!room.data) {
             throw new Error("Tried to upsert a room with undefined data");
         }
diff --git a/src/db/schema/v7.ts b/src/db/schema/v7.ts
index 879b34e1ecde331c5839f8275b58a301dfc0336b..9caaa668a09e008b62f2ae97d2d03921097dfa01 100644
--- a/src/db/schema/v7.ts
+++ b/src/db/schema/v7.ts
@@ -29,8 +29,8 @@ export class Schema implements IDbSchema {
                 name TEXT NOT NULL,
                 animated INTEGER NOT NULL,
                 mxc_url TEXT NOT NULL,
-                created_at INTEGER NOT NULL,
-                updated_at INTEGER NOT NULL,
+                created_at BIGINT NOT NULL,
+                updated_at BIGINT NOT NULL,
                 PRIMARY KEY(emoji_id)
         );`, "emoji");
 
diff --git a/src/db/schema/v9.ts b/src/db/schema/v9.ts
index dbb111d44aa022875fff72413f670203659bcf1a..abb63e4448d4f3dd5c4d6b56e8a2973d5e5d296c 100644
--- a/src/db/schema/v9.ts
+++ b/src/db/schema/v9.ts
@@ -17,9 +17,6 @@ limitations under the License.
 import { IDbSchema } from "./dbschema";
 import { DiscordStore } from "../../store";
 import { Log } from "../../log";
-import { RemoteUser } from "../userstore";
-import * as Queue from "p-queue";
-const log = new Log("SchemaV9");
 
 export class Schema implements IDbSchema {
     public description = "create user store tables";
diff --git a/src/db/userstore.ts b/src/db/userstore.ts
index cb1251e28c1f08d86e74ba47aa7695da00319474..7e84dc0a84faf4767a188349e11236ef34586d9c 100644
--- a/src/db/userstore.ts
+++ b/src/db/userstore.ts
@@ -15,8 +15,9 @@ limitations under the License.
 */
 
 import { IDatabaseConnector } from "./connector";
-import * as uuid from "uuid/v4";
 import { Log } from "../log";
+import { MetricPeg } from "../metrics";
+import { TimedCache } from "../structures/timedcache";
 
 /**
  * A UserStore compatible with
@@ -45,17 +46,20 @@ export interface IUserStoreEntry {
 }
 
 export class DbUserStore {
-    private remoteUserCache: Map<string, {e: RemoteUser, ts: number}>;
+    private remoteUserCache: TimedCache<string, RemoteUser>;
 
     constructor(private db: IDatabaseConnector) {
-        this.remoteUserCache = new Map();
+        this.remoteUserCache = new TimedCache(ENTRY_CACHE_LIMETIME);
     }
 
     public async getRemoteUser(remoteId: string): Promise<RemoteUser|null> {
         const cached = this.remoteUserCache.get(remoteId);
-        if (cached && cached.ts + ENTRY_CACHE_LIMETIME > Date.now()) {
-            return cached.e;
+        if (cached) {
+            MetricPeg.get.storeCall("UserStore.getRemoteUser", true);
+            return cached;
         }
+        MetricPeg.get.storeCall("UserStore.getRemoteUser", false);
+
         const row = await this.db.Get(
             "SELECT * FROM user_entries WHERE remote_id = $id", {id: remoteId},
         );
@@ -81,11 +85,12 @@ export class DbUserStore {
                 remoteUser.guildNicks.set(guild_id as string, nick as string);
             });
         }
-        this.remoteUserCache.set(remoteId, {e: remoteUser, ts: Date.now()});
+        this.remoteUserCache.set(remoteId, remoteUser);
         return remoteUser;
     }
 
     public async setRemoteUser(user: RemoteUser) {
+        MetricPeg.get.storeCall("UserStore.setRemoteUser", false);
         this.remoteUserCache.delete(user.id);
         const existingData = await this.db.Get(
             "SELECT * FROM remote_user_data WHERE remote_id = $remoteId",
@@ -156,6 +161,7 @@ AND guild_id = $guild_id`,
     }
 
     public async linkUsers(matrixId: string, remoteId: string) {
+        MetricPeg.get.storeCall("UserStore.linkUsers", false);
         // This is used  ONCE in the bridge to link two IDs, so do not UPSURT data.
         try {
             await this.db.Run(`INSERT INTO user_entries VALUES ($matrixId, $remoteId)`, {
diff --git a/src/discordas.ts b/src/discordas.ts
index 9d72fbae05cf9b85883b73aca571caa022e33694..375f116e895c9ffb4c2d6f233d2e6c9954a7170c 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -13,7 +13,7 @@ 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 { Appservice, IAppserviceRegistration, ConsoleLogger, LogService } from "matrix-bot-sdk";
+import { Appservice, IAppserviceRegistration, LogService } from "matrix-bot-sdk";
 import * as yaml from "js-yaml";
 import * as fs from "fs";
 import { DiscordBridgeConfig } from "./config";
@@ -25,6 +25,7 @@ import * as cliArgs from "command-line-args";
 import * as usage from "command-line-usage";
 import * as uuid from "uuid/v4";
 import { IMatrixEvent } from "./matrixtypes";
+import { MetricPeg, PrometheusBridgeMetrics } from "./metrics";
 
 const log = new Log("DiscordAS");
 
@@ -46,17 +47,17 @@ function generateRegistration(opts, registrationPath)  {
         hs_token: uuid(),
         id: "discord-bridge",
         namespaces: {
-            users: [
+            aliases: [
                 {
                     exclusive: true,
-                    regex: "@_discord_.*",
+                    regex: "#_discord_.*",
                 },
             ],
             rooms: [ ],
-            aliases: [
+            users: [
                 {
                     exclusive: true,
-                    regex: "#_discord_.*",
+                    regex: "@_discord_.*",
                 },
             ],
         },
@@ -68,6 +69,33 @@ function generateRegistration(opts, registrationPath)  {
     fs.writeFileSync(registrationPath, yaml.safeDump(reg));
 }
 
+function setupLogging() {
+    const logMap = new Map<string, Log>();
+    const logFunc = (level: string, module: string, args: any[]) => {
+        if (!Array.isArray(args)) {
+            args = [args];
+        }
+        if (args.find((s) => s.includes && s.includes("M_USER_IN_USE"))) {
+            // Spammy logs begon
+            return;
+        }
+        const mod =  "bot-sdk" + module;
+        let logger = logMap.get(mod);
+        if (!logger) {
+            logger = new Log(mod);
+            logMap.set(mod, logger);
+        }
+        logger[level](args);
+    };
+
+    LogService.setLogger({
+        debug: (mod: string, args: any[]) => logFunc("silly", mod, args),
+        error: (mod: string, args: any[]) => logFunc("error", mod, args),
+        info: (mod: string, args: any[]) => logFunc("info", mod, args),
+        warn: (mod: string, args: any[]) => logFunc("warn", mod, args),
+    });
+}
+
 async function run() {
     const opts = cliArgs(commandOptions);
     if (opts.help) {
@@ -101,9 +129,10 @@ async function run() {
     if (!port) {
         throw Error("Port not given in command line or config file");
     }
-    config.ApplyConfig(yaml.safeLoad(fs.readFileSync(configPath, "utf8")));
+    config.applyConfig(yaml.safeLoad(fs.readFileSync(configPath, "utf8")));
     Log.Configure(config.logging);
     const registration = yaml.safeLoad(fs.readFileSync(registrationPath, "utf8")) as IAppserviceRegistration;
+    setupLogging();
     const appservice = new Appservice({
         bindAddress: config.bridge.bindAddress || "0.0.0.0",
         homeserverName: config.bridge.domain,
@@ -112,31 +141,6 @@ async function run() {
         registration,
     });
 
-    const logMap = new Map<string, Log>();
-    const logFunc = (level: string, module: string, args: any[]) => {
-        if (!Array.isArray(args)) {
-            args = [args];
-        }
-        if (args.find((s) => s.includes && s.includes("M_USER_IN_USE"))) {
-            // Spammy logs begon
-            return;
-        }
-        const mod =  "bot-sdk" + module;
-        let logger = logMap.get(mod);
-        if (!logger) {
-            logger = new Log(mod);
-            logMap.set(mod, logger);
-        }
-        logger[level](args);
-    };
-
-    LogService.setLogger({
-        debug: (mod: string, args: any[]) => logFunc("silly", mod, args),
-        error: (mod: string, args: any[]) => logFunc("error", mod, args),
-        info: (mod: string, args: any[]) => logFunc("info", mod, args),
-        warn: (mod: string, args: any[]) => logFunc("warn", mod, args),
-    });
-
     const store = new DiscordStore(config.database);
 
     if (config.database.roomStorePath) {
@@ -149,6 +153,12 @@ async function run() {
                + "The config option userStorePath no longer has any use.");
     }
 
+    if (config.bridge.enableMetrics) {
+        log.info("Enabled metrics");
+        MetricPeg.set(new PrometheusBridgeMetrics().init());
+    }
+
+
     try {
         await store.init();
     } catch (ex) {
@@ -192,4 +202,4 @@ async function run() {
 run().catch((err) => {
     log.error("A fatal error occurred during startup:", err);
     process.exit(1);
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/src/log.ts b/src/log.ts
index c525f7c79842f34934485ba8a809f703db686607..0477d8a1af5e77d3e47289f56a034a6afc3248e3 100644
--- a/src/log.ts
+++ b/src/log.ts
@@ -17,7 +17,6 @@ limitations under the License.
 import { createLogger, Logger, format, transports } from "winston";
 import { DiscordBridgeConfigLogging, LoggingFile} from "./config";
 import { inspect } from "util";
-import * as moment from "moment";
 import "winston-daily-rotate-file";
 
 const FORMAT_FUNC = format.printf((info) => {
@@ -47,10 +46,6 @@ export class Log {
     private static config: DiscordBridgeConfigLogging;
     private static logger: Logger;
 
-    private static now() {
-        return moment().format(Log.config.lineDateFormat);
-    }
-
     private static setupLogger() {
         if (Log.logger) {
             Log.logger.close();
@@ -64,7 +59,7 @@ export class Log {
         Log.logger = createLogger({
             format: format.combine(
                 format.timestamp({
-                    format: Log.now(),
+                    format: Log.config.lineDateFormat,
                 }),
                 format.colorize(),
                 FORMAT_FUNC,
diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts
index a65cf805dc08149d3499abb04fafeb22ffb39eb7..4f1c3cb02f32b39c3a47ea36691048c5ff7611b5 100644
--- a/src/matrixeventprocessor.ts
+++ b/src/matrixeventprocessor.ts
@@ -27,6 +27,8 @@ import { Log } from "./log";
 import { IRoomStoreEntry } from "./db/roomstore";
 import { Appservice, MatrixClient } from "matrix-bot-sdk";
 import { DiscordStore } from "./store";
+import { TimedCache } from "./structures/timedcache";
+import { MetricPeg } from "./metrics";
 
 const log = new Log("MatrixEventProcessor");
 
@@ -37,6 +39,7 @@ const DISCORD_AVATAR_WIDTH = 128;
 const DISCORD_AVATAR_HEIGHT = 128;
 const ROOM_NAME_PARTS = 2;
 const AGE_LIMIT = 900000; // 15 * 60 * 1000
+const PROFILE_CACHE_LIFETIME = 900000;
 
 export class MatrixEventProcessorOpts {
     constructor(
@@ -61,6 +64,7 @@ export class MatrixEventProcessor {
     private store: DiscordStore;
     private matrixMsgProcessor: MatrixMessageProcessor;
     private mxCommandHandler: MatrixCommandHandler;
+    private mxUserProfileCache: TimedCache<string, {displayname: string, avatar_url: string|undefined}>;
 
     constructor(opts: MatrixEventProcessorOpts, cm?: MatrixCommandHandler) {
         this.config = opts.config;
@@ -68,6 +72,7 @@ export class MatrixEventProcessor {
         this.discord = opts.discord;
         this.store = opts.store;
         this.matrixMsgProcessor = new MatrixMessageProcessor(this.discord, this.config.bridge.homeserverUrl);
+        this.mxUserProfileCache = new TimedCache(PROFILE_CACHE_LIFETIME);
         if (cm) {
             this.mxCommandHandler = cm;
         } else {
@@ -79,6 +84,7 @@ export class MatrixEventProcessor {
         const remoteRoom = rooms[0];
         if (event.unsigned.age > AGE_LIMIT) {
             log.warn(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`);
+            MetricPeg.get.requestOutcome(event.event_id, false, "dropped");
             return;
         }
         if (
@@ -130,12 +136,11 @@ export class MatrixEventProcessor {
                 const srvChanPair = remoteRoom.remote!.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS);
                 try {
                     await this.ProcessMsgEvent(event, srvChanPair[0], srvChanPair[1]);
-                    return;
                 } catch (err) {
                     log.warn("There was an error sending a matrix event", err);
-                    return;
                 }
             }
+            return;
         } else if (event.type === "m.room.encryption" && remoteRoom) {
             try {
                 await this.HandleEncryptionWarning(event.room_id);
@@ -143,10 +148,9 @@ export class MatrixEventProcessor {
             } catch (err) {
                 throw new Error(`Failed to handle encrypted room, ${err}`);
             }
-        } else {
-            log.verbose("Got non m.room.message event");
         }
         log.verbose("Event not processed by bridge");
+        MetricPeg.get.requestOutcome(event.event_id, false, "dropped");
     }
 
     public async HandleEncryptionWarning(roomId: string): Promise<void> {
@@ -172,7 +176,6 @@ export class MatrixEventProcessor {
         log.verbose(`Looking up ${guildId}_${channelId}`);
         const roomLookup = await this.discord.LookupRoom(guildId, channelId, event.sender);
         const chan = roomLookup.channel;
-        const botUser = roomLookup.botUser;
 
         const embedSet = await this.EventToEmbed(event, chan);
         const opts: Discord.MessageOptions = {};
@@ -184,7 +187,10 @@ export class MatrixEventProcessor {
         }
 
         await this.discord.send(embedSet, opts, roomLookup, event);
-        await this.sendReadReceipt(event);
+        // Don't await this.
+        this.sendReadReceipt(event).catch((ex) => {
+            log.verbose("Failed to send read reciept for ", event.event_id, ex);
+        });
     }
 
     public async ProcessStateEvent(event: IMatrixEvent) {
@@ -204,31 +210,42 @@ export class MatrixEventProcessor {
 
         let msg = `\`${event.sender}\` `;
 
+        const allowJoinLeave = !this.config.bridge.disableJoinLeaveNotifications;
+
         if (event.type === "m.room.name") {
             msg += `set the name to \`${event.content!.name}\``;
         } else if (event.type === "m.room.topic") {
             msg += `set the topic to \`${event.content!.topic}\``;
         } else if (event.type === "m.room.member") {
             const membership = event.content!.membership;
-            if (membership === "join"
-                && event.unsigned.prev_content === undefined) {
-                    if (!this.config.bridge.disableJoinLeaveNotifications) {
-                        msg += `joined the room`;
-                    } else {
-                        return;
-                    }
+            const client = this.bridge.botIntent.underlyingClient;
+            const isNewJoin = event.unsigned.replaces_state === undefined ? true : (
+                await client.getEvent(event.room_id, event.unsigned.replaces_state)).content.membership !== "join";
+            if (membership === "join") {
+                this.mxUserProfileCache.delete(`${event.room_id}:${event.sender}`);
+                this.mxUserProfileCache.delete(event.sender);
+                if (event.content!.displayname) {
+                    this.mxUserProfileCache.set(`${event.room_id}:${event.sender}`, {
+                        avatar_url: event.content!.avatar_url,
+                        displayname: event.content!.displayname!,
+                    });
+                }
+                // We don't know if the user also updated their profile, but to be safe..
+                this.mxUserProfileCache.delete(event.sender);
+            }
+            if (membership === "join" && isNewJoin && allowJoinLeave) {
+                msg += "joined the room";
             } else if (membership === "invite") {
                 msg += `invited \`${event.state_key}\` to the room`;
             } else if (membership === "leave" && event.state_key !== event.sender) {
                 msg += `kicked \`${event.state_key}\` from the room`;
-            } else if (membership === "leave") {
-                if (!this.config.bridge.disableJoinLeaveNotifications) {
-                    msg += `left the room`;
-                } else {
-                    return;
-                }
+            } else if (membership === "leave" && allowJoinLeave) {
+                msg += "left the room";
             } else if (membership === "ban") {
                 msg += `banned \`${event.state_key}\` from the room`;
+            } else {
+                // Ignore anything else
+                return;
             }
         }
 
@@ -241,19 +258,7 @@ export class MatrixEventProcessor {
         event: IMatrixEvent, channel: Discord.TextChannel, getReply: boolean = true,
     ): Promise<IMatrixEventProcessorResult> {
         const mxClient = this.bridge.botIntent.underlyingClient;
-        let profile: IMatrixEvent | null = null;
-        try {
-            profile = await mxClient.getRoomStateEvent(event.room_id, "m.room.member", event.sender);
-            if (!profile) {
-                profile = await mxClient.getUserProfile(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);
-        }
-
+        const profile = await this.GetUserProfileForRoom(event.room_id, event.sender);
         const params = {
             mxClient,
             roomId: event.room_id,
@@ -377,6 +382,44 @@ export class MatrixEventProcessor {
         return embed;
     }
 
+    private async GetUserProfileForRoom(roomId: string, userId: string) {
+        const mxClient = this.bridge.botIntent.underlyingClient;
+        let profile: {displayname: string, avatar_url: string|undefined} | undefined;
+        try {
+            // First try to pull out the room-specific profile from the cache.
+            profile = this.mxUserProfileCache.get(`${roomId}:${userId}`);
+            if (profile) {
+                return profile;
+            }
+            log.verbose(`Profile ${userId}:${roomId} not cached`);
+
+            // Failing that, try fetching the state.
+            profile = await mxClient.getRoomStateEvent(roomId, "m.room.member", userId);
+            if (profile) {
+                this.mxUserProfileCache.set(`${roomId}:${userId}`, profile);
+                return profile;
+            }
+
+            // Try fetching the users profile from the cache
+            profile = this.mxUserProfileCache.get(userId);
+            if (profile) {
+                return profile;
+            }
+
+            // Failing that, try fetching the profile.
+            log.verbose(`Profile ${userId} not cached`);
+            profile = await mxClient.getUserProfile(userId);
+            if (profile) {
+                this.mxUserProfileCache.set(userId, profile);
+                return profile;
+            }
+            log.warn(`User ${userId} has no member state and no profile. That's odd.`);
+        } catch (err) {
+            log.warn(`Trying to fetch member state or profile for ${userId} failed`, err);
+        }
+        return undefined;
+    }
+
     private async sendReadReceipt(event: IMatrixEvent) {
         if (!this.config.bridge.disableReadReceipts) {
             try {
@@ -404,8 +447,9 @@ export class MatrixEventProcessor {
         return hasAttachment;
     }
 
-    private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: IMatrixEvent | null) {
-        const intent = this.bridge.botIntent;
+    private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: {
+        displayname: string,
+        avatar_url: string|undefined }) {
         let displayName = sender;
         let avatarUrl;
 
@@ -428,13 +472,6 @@ export class MatrixEventProcessor {
             }
             // Let it fall through.
         }
-        if (!profile) {
-            try {
-                profile = await intent.underlyingClient.getUserProfile(sender);
-            } catch (ex) {
-                log.warn(`Failed to fetch profile for ${sender}`, ex);
-            }
-        }
 
         if (profile) {
             if (profile.displayname &&
diff --git a/src/matrixtypes.ts b/src/matrixtypes.ts
index 7535b59a89063a02e1f1946321550287f483dd94..f08ae1301c571fdbc866e7aafff94eed21e8229f 100644
--- a/src/matrixtypes.ts
+++ b/src/matrixtypes.ts
@@ -23,6 +23,7 @@ export interface IMatrixEventContent {
     msgtype?: string;
     url?: string;
     displayname?: string;
+    avatar_url?: string;
     reason?: string;
     "m.relates_to"?: any; // tslint:disable-line no-any
 }
diff --git a/src/metrics.ts b/src/metrics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e228a11f64b1d0f2d38e755615552399f413a0f
--- /dev/null
+++ b/src/metrics.ts
@@ -0,0 +1,155 @@
+/*
+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 { Gauge, Counter, Histogram } from "prom-client";
+import { Log } from "./log";
+import { Appservice } from "matrix-bot-sdk";
+
+const AgeCounters = PrometheusMetrics.AgeCounters;
+const log = new Log("BridgeMetrics");
+const REQUEST_EXPIRE_TIME_MS = 30000;
+
+interface IAgeCounter {
+    setGauge(gauge: Gauge, morelabels: string[]);
+    bump(age: number);
+}
+
+interface IBridgeGauges {
+    matrixRoomConfigs: number;
+    remoteRoomConfigs: number;
+    matrixGhosts: number;
+    remoteGhosts: number;
+    matrixRoomsByAge: IAgeCounter;
+    remoteRoomsByAge: IAgeCounter;
+    matrixUsersByAge: IAgeCounter;
+    remoteUsersByAge: IAgeCounter;
+}
+
+export interface IBridgeMetrics {
+    registerRequest(id: string);
+    requestOutcome(id: string, isRemote: boolean, outcome: string);
+    remoteCall(method: string);
+    setPresenceCount(count: number);
+    storeCall(method: string, cached: boolean);
+}
+
+export class DummyBridgeMetrics implements IBridgeMetrics {
+    public registerRequest() {}
+    public requestOutcome() {}
+    public remoteCall() {}
+    public setPresenceCount() {}
+    public storeCall() {}
+}
+
+export class MetricPeg {
+    public static get get(): IBridgeMetrics {
+        return this.metrics;
+    }
+
+    public static set(metrics: IBridgeMetrics) {
+        this.metrics = metrics;
+    }
+
+    private static metrics: IBridgeMetrics = new DummyBridgeMetrics();
+}
+
+export class PrometheusBridgeMetrics implements IBridgeMetrics {
+    private metrics;
+    private remoteCallCounter: Counter;
+    private storeCallCounter: Counter;
+    private presenceGauge: Gauge;
+    private remoteRequest: Histogram;
+    private matrixRequest: Histogram;
+    private requestsInFlight: Map<string, number>;
+    private bridgeGauges: IBridgeGauges = {
+        matrixGhosts: 0,
+        matrixRoomConfigs: 0,
+        matrixRoomsByAge: new AgeCounters(),
+        matrixUsersByAge: new AgeCounters(),
+        remoteGhosts: 0,
+        remoteRoomConfigs: 0,
+        remoteRoomsByAge: new AgeCounters(),
+        remoteUsersByAge: new AgeCounters(),
+    };
+
+    public init(as: Appservice) {
+        this.metrics = new PrometheusMetrics();
+        this.metrics.registerMatrixSdkMetrics();
+        this.metrics.registerBridgeGauges(() => this.bridgeGauges);
+        this.metrics.addAppServicePath(bridge);
+        this.remoteCallCounter = this.metrics.addCounter({
+            help: "Count of remote API calls made",
+            labels: ["method"],
+            name: "remote_api_calls",
+        });
+        this.storeCallCounter = this.metrics.addCounter({
+            help: "Count of store function calls made",
+            labels: ["method", "cached"],
+            name: "store_calls",
+        });
+        this.presenceGauge = this.metrics.addGauge({
+            help: "Count of users in the presence queue",
+            labels: [],
+
+            name: "active_presence_users",
+        });
+        this.matrixRequest = this.metrics.addTimer({
+            help: "Histogram of processing durations of received Matrix messages",
+            labels: ["outcome"],
+            name: "matrix_request_seconds",
+        });
+        this.remoteRequest = this.metrics.addTimer({
+            help: "Histogram of processing durations of received remote messages",
+            labels: ["outcome"],
+            name: "remote_request_seconds",
+        });
+        this.requestsInFlight = new Map();
+        setInterval(() => {
+            this.requestsInFlight.forEach((time, id) => {
+                if (Date.now() - time) {
+                    this.requestsInFlight.delete(id);
+                }
+            });
+        }, REQUEST_EXPIRE_TIME_MS);
+        return this;
+    }
+
+    public registerRequest(id: string) {
+        this.requestsInFlight.set(id, Date.now());
+    }
+
+    public requestOutcome(id: string, isRemote: boolean, outcome: string) {
+        const startTime = this.requestsInFlight.get(id);
+        if (!startTime) {
+            return;
+        }
+        this.requestsInFlight.delete(id);
+        const duration = Date.now() - startTime;
+        (isRemote ? this.remoteRequest : this.matrixRequest).observe({outcome}, duration / 1000);
+    }
+
+    public setPresenceCount(count: number) {
+        this.presenceGauge.set(count);
+    }
+
+    public remoteCall(method: string) {
+        this.remoteCallCounter.inc({method});
+    }
+
+    public storeCall(method: string, cached: boolean) {
+        this.storeCallCounter.inc({method, cached: cached ? "yes" : "no"});
+    }
+}
diff --git a/src/presencehandler.ts b/src/presencehandler.ts
index d4d0fc364c8ca36b5cd0bdce528eae0397abe663..9e064ab33142e60925f342ca249f60ea4ee23918 100644
--- a/src/presencehandler.ts
+++ b/src/presencehandler.ts
@@ -17,6 +17,7 @@ limitations under the License.
 import { User, Presence } from "discord.js";
 import { DiscordBot } from "./bot";
 import { Log } from "./log";
+import { MetricPeg } from "./metrics";
 const log = new Log("PresenceHandler");
 
 export class PresenceHandlerStatus {
@@ -65,6 +66,7 @@ export class PresenceHandler {
         if (user.id !== this.bot.GetBotId() && this.presenceQueue.find((u) => u.id === user.id) === undefined) {
             log.verbose(`Adding ${user.id} (${user.username}) to the presence queue`);
             this.presenceQueue.push(user);
+            MetricPeg.get.setPresenceCount(this.presenceQueue.length);
         }
     }
 
@@ -74,6 +76,7 @@ export class PresenceHandler {
         });
         if (index !== -1) {
             this.presenceQueue.splice(index, 1);
+            MetricPeg.get.setPresenceCount(this.presenceQueue.length);
         } else {
             log.warn(
                 `Tried to remove ${user.id} from the presence queue but it could not be found`,
@@ -95,6 +98,7 @@ export class PresenceHandler {
                 this.presenceQueue.push(user);
             } else {
                 log.verbose(`Dropping ${user.id} from the presence queue.`);
+                MetricPeg.get.setPresenceCount(this.presenceQueue.length);
             }
         }
     }
diff --git a/src/store.ts b/src/store.ts
index 7793e8190c78f7ec76cfb2446f27c2094cb5de8e..ddb35cd2d9cc2e87e3855671fbf4305330cb809b 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -68,7 +68,7 @@ export class DiscordStore {
                 return resolve(err === null);
             });
         }).then(async (result) => {
-            return new Promise((resolve, reject) => {
+            return new Promise<void|{}>((resolve, reject) => {
                 if (!result) {
                     log.warn("NOT backing up database while a file already exists");
                     resolve(true);
@@ -226,7 +226,6 @@ export class DiscordStore {
             throw err;
         }
     }
-    
     // tslint:disable-next-line no-any
     public async Get<T extends IDbData>(dbType: {new(): T; }, params: any): Promise<T|null> {
         const dType = new dbType();
diff --git a/src/structures/timedcache.ts b/src/structures/timedcache.ts
new file mode 100644
index 0000000000000000000000000000000000000000..93424af9d488d493439bd4294dc969297f79eeee
--- /dev/null
+++ b/src/structures/timedcache.ts
@@ -0,0 +1,106 @@
+interface ITimedValue<V> {
+    value: V;
+    ts: number;
+}
+
+export class TimedCache<K, V> implements Map<K, V> {
+    private readonly  map: Map<K, ITimedValue<V>>;
+
+    public constructor(private readonly liveFor: number) {
+        this.map = new Map();
+    }
+
+    public clear(): void {
+        this.map.clear();
+    }
+
+    public delete(key: K): boolean {
+        return this.map.delete(key);
+    }
+
+    public forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void|Promise<void>): void {
+        for (const item of this) {
+            callbackfn(item[1], item[0], this);
+        }
+    }
+
+    public get(key: K): V | undefined {
+        const v = this.map.get(key);
+        if (v === undefined) {
+            return;
+        }
+        const val = this.filterV(v);
+        if (val !== undefined) {
+            return val;
+        }
+        // Cleanup expired key
+        this.map.delete(key);
+    }
+
+    public has(key: K): boolean {
+        return this.get(key) !== undefined;
+    }
+
+    public set(key: K, value: V): this {
+        this.map.set(key, {
+            ts: Date.now(),
+            value,
+        });
+        return this;
+    }
+
+    public get size(): number {
+        return this.map.size;
+    }
+
+    public [Symbol.iterator](): IterableIterator<[K, V]> {
+        let iterator: IterableIterator<[K, ITimedValue<V>]>;
+        return {
+            next: () => {
+                if (!iterator) {
+                    iterator = this.map.entries();
+                }
+                let item: IteratorResult<[K, ITimedValue<V>]>|undefined;
+                let filteredValue: V|undefined;
+                // Loop if we have no item, or the item has expired.
+                while (!item || filteredValue === undefined) {
+                    item = iterator.next();
+                    // No more items in map. Bye bye.
+                    if (item.done) {
+                        break;
+                    }
+                    filteredValue = this.filterV(item.value[1]);
+                }
+                if (item.done) {
+                    // Typscript doesn't like us returning undefined for value, which is dumb.
+                    // tslint:disable-next-line: no-any
+                    return {done: true, value: undefined} as any as IteratorResult<[K, V]>;
+                }
+                return {done: false, value: [item.value[0], filteredValue]} as IteratorResult<[K, V]>;
+            },
+            [Symbol.iterator]: () => this[Symbol.iterator](),
+        };
+    }
+
+    public entries(): IterableIterator<[K, V]> {
+        return this[Symbol.iterator]();
+    }
+
+    public keys(): IterableIterator<K> {
+        throw new Error("Method not implemented.");
+    }
+
+    public values(): IterableIterator<V> {
+        throw new Error("Method not implemented.");
+    }
+
+    get [Symbol.toStringTag](): "Map" {
+        return "Map";
+    }
+
+    private filterV(v: ITimedValue<V>): V|undefined {
+        if (Date.now() - v.ts < this.liveFor) {
+            return v.value;
+        }
+    }
+}
diff --git a/test/config.ts b/test/config.ts
index b6d7f447d8cf09471281abc3d3c84107f5fe1b2b..13b2ea444cf91e349167587d68f836afcf5252c1 100644
--- a/test/config.ts
+++ b/test/config.ts
@@ -18,11 +18,6 @@ import { argv } from "process";
 import { Log } from "../src/log";
 import * as WhyRunning from "why-is-node-running";
 
-const logger = new Log("MessageProcessor");
-
-// we are a test file and thus need those
-/* tslint:disable:no-unused-expression max-file-line-count */
-
 if (!argv.includes("--noisy")) {
     Log.ForceSilent();
 }
diff --git a/test/mocks/discordclientfactory.ts b/test/mocks/discordclientfactory.ts
index 803dedc118f7cacbb9c44443840dee85c06c4c4b..b174f3d5f9455581b13c2a3460f4eee4a32caa7d 100644
--- a/test/mocks/discordclientfactory.ts
+++ b/test/mocks/discordclientfactory.ts
@@ -31,4 +31,6 @@ export class DiscordClientFactory {
         }
         return this.botClient;
     }
+
+    public bindMetricsToChannel() {}
 }
diff --git a/test/structures/test_timedcache.ts b/test/structures/test_timedcache.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f002fc77ee182cb14f839c52a39f3228ce02ef67
--- /dev/null
+++ b/test/structures/test_timedcache.ts
@@ -0,0 +1,124 @@
+/*
+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 { TimedCache } from "../../src/structures/timedcache";
+import { Util } from "../../src/util";
+
+// we are a test file and thus need those
+/* tslint:disable:no-unused-expression max-file-line-count no-any */
+
+describe("TimedCache", () => {
+    it("should construct", () => {
+        const timedCache = new TimedCache<string, number>(1000);
+        expect(timedCache.size).to.equal(0);
+    });
+
+    it("should add and get values", () => {
+        const timedCache = new TimedCache<string, number>(1000);
+        timedCache.set("foo", 1);
+        timedCache.set("bar", -1);
+        timedCache.set("baz", 0);
+        expect(timedCache.get("foo")).to.equal(1);
+        expect(timedCache.get("bar")).to.equal(-1);
+        expect(timedCache.get("baz")).to.equal(0);
+    });
+
+    it("should be able to overwrite values", () => {
+        const timedCache = new TimedCache<string, number>(1000);
+        timedCache.set("foo", 1);
+        expect(timedCache.get("foo")).to.equal(1);
+        timedCache.set("bar", 0);
+        timedCache.set("foo", -1);
+        expect(timedCache.get("bar")).to.equal(0);
+        expect(timedCache.get("foo")).to.equal(-1);
+    });
+
+    it("should be able to check if a value exists", () => {
+        const timedCache = new TimedCache<string, number>(1000);
+        expect(timedCache.has("foo")).to.be.false;
+        timedCache.set("foo", 1);
+        expect(timedCache.has("foo")).to.be.true;
+        timedCache.set("bar", 1);
+        expect(timedCache.has("bar")).to.be.true;
+    });
+
+    it("should be able to delete a value", () => {
+        const timedCache = new TimedCache<string, number>(1000);
+        timedCache.set("foo", 1);
+        expect(timedCache.has("foo")).to.be.true;
+        timedCache.delete("foo");
+        expect(timedCache.has("foo")).to.be.false;
+        expect(timedCache.get("foo")).to.be.undefined;
+    });
+
+    it("should expire a value", async () => {
+        const LIVE_FOR = 50;
+        const timedCache = new TimedCache<string, number>(LIVE_FOR);
+        timedCache.set("foo", 1);
+        expect(timedCache.has("foo")).to.be.true;
+        expect(timedCache.get("foo")).to.equal(1);
+        await Util.DelayedPromise(LIVE_FOR);
+        expect(timedCache.has("foo")).to.be.false;
+        expect(timedCache.get("foo")).to.be.undefined;
+    });
+
+    it("should be able to iterate around a long-lasting collection", () => {
+        const timedCache = new TimedCache<string, number>(1000);
+        timedCache.set("foo", 1);
+        timedCache.set("bar", -1);
+        timedCache.set("baz", 0);
+        let i = 0;
+        for (const iterator of timedCache) {
+            if (i === 0) {
+                expect(iterator[0]).to.equal("foo");
+                expect(iterator[1]).to.equal(1);
+            } else if (i === 1) {
+                expect(iterator[0]).to.equal("bar");
+                expect(iterator[1]).to.equal(-1);
+            } else {
+                expect(iterator[0]).to.equal("baz");
+                expect(iterator[1]).to.equal(0);
+            }
+            i++;
+        }
+    });
+
+    it("should be able to iterate around a short-term collection", async () => {
+        const LIVE_FOR = 100;
+        const timedCache = new TimedCache<string, number>(LIVE_FOR);
+        timedCache.set("foo", 1);
+        timedCache.set("bar", -1);
+        timedCache.set("baz", 0);
+        let i = 0;
+        for (const iterator of timedCache) {
+            if (i === 0) {
+                expect(iterator[0]).to.equal("foo");
+                expect(iterator[1]).to.equal(1);
+            } else if (i === 1) {
+                expect(iterator[0]).to.equal("bar");
+                expect(iterator[1]).to.equal(-1);
+            } else {
+                expect(iterator[0]).to.equal("baz");
+                expect(iterator[1]).to.equal(0);
+            }
+            i++;
+        }
+        await Util.DelayedPromise(LIVE_FOR);
+        const vals = [...timedCache.entries()];
+        expect(vals).to.be.empty;
+    });
+});
diff --git a/test/test_channelsyncroniser.ts b/test/test_channelsyncroniser.ts
index 67fdeb311a375e5bf9a5b2bd2929cdcb7ecca21f..50308d9dad89ee8e7a0be3a30c0395cb4039e562 100644
--- a/test/test_channelsyncroniser.ts
+++ b/test/test_channelsyncroniser.ts
@@ -387,6 +387,33 @@ describe("ChannelSyncroniser", () => {
             expect(state.mxChannels[0].iconUrl).equals("https://cdn.discordapp.com/icons/654321/new_icon.png");
             expect(state.mxChannels[0].iconId).equals("new_icon");
         });
+        it("will update animated icons", async () => {
+            const guild = new MockGuild("654321", [], "newGuild");
+            guild.icon = "a_new_icon";
+            const chan = new MockChannel();
+            chan.type = "text";
+            chan.id = "blah";
+            chan.guild = guild;
+
+            const testStore = [
+                new Entry({
+                    id: "1",
+                    matrix_id: "!1:localhost",
+                    remote: {
+                        discord_channel: chan.id,
+                        discord_iconurl: "https://cdn.discordapp.com/icons/654321/old_icon.png",
+                        update_icon: true,
+                    },
+                    remote_id: "111",
+                }),
+            ];
+
+            const channelSync = CreateChannelSync(testStore);
+            const state = await channelSync.GetChannelUpdateState(chan as any);
+            expect(state.mxChannels.length).equals(1);
+            expect(state.mxChannels[0].iconUrl).equals("https://cdn.discordapp.com/icons/654321/a_new_icon.gif");
+            expect(state.mxChannels[0].iconId).equals("a_new_icon");
+        });
         it("won't update the icon", async () => {
             const guild = new MockGuild("654321", [], "newGuild");
             guild.icon = "new_icon";
diff --git a/test/test_config.ts b/test/test_config.ts
index 0979897b55ea157ff94e02386674097ab0516645..badb662a5c4a9929bfdfacb22c45faeef09a965d 100644
--- a/test/test_config.ts
+++ b/test/test_config.ts
@@ -22,10 +22,10 @@ import { DiscordBridgeConfig } from "../src/config";
 
 const expect = Chai.expect;
 
-describe("DiscordBridgeConfig.ApplyConfig", () => {
+describe("DiscordBridgeConfig.applyConfig", () => {
     it("should merge configs correctly", () => {
         const config = new DiscordBridgeConfig();
-        config.ApplyConfig({
+        config.applyConfig({
             bridge: {
                 disableDeletionForwarding: true,
                 disableDiscordMentions: false,
@@ -38,17 +38,40 @@ describe("DiscordBridgeConfig.ApplyConfig", () => {
                 console: "warn",
             },
         });
-        expect(config.bridge.homeserverUrl, "blah");
+        expect(config.bridge.homeserverUrl).to.equal("blah");
         expect(config.bridge.disableTypingNotifications).to.be.true;
         expect(config.bridge.disableDiscordMentions).to.be.false;
         expect(config.bridge.disableDeletionForwarding).to.be.true;
         expect(config.bridge.enableSelfServiceBridging).to.be.false;
         expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
-        expect(config.logging.console, "warn");
+        expect(config.logging.console).to.equal("warn");
+    });
+    it("should merge environment overrides correctly", () => {
+        const config = new DiscordBridgeConfig();
+        config.applyConfig({
+            bridge: {
+                disableDeletionForwarding: true,
+                disableDiscordMentions: false,
+                homeserverUrl: "blah",
+            },
+            logging: {
+                console: "warn",
+            },
+        });
+        config.applyEnvironmentOverrides({
+            APPSERVICE_DISCORD_BRIDGE_DISABLE_DELETION_FORWARDING: false,
+            APPSERVICE_DISCORD_BRIDGE_DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
+            APPSERVICE_DISCORD_LOGGING_CONSOLE: "debug",
+        });
+        expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
+        expect(config.bridge.disableDeletionForwarding).to.be.false;
+        expect(config.bridge.disableDiscordMentions).to.be.false;
+        expect(config.bridge.homeserverUrl).to.equal("blah");
+        expect(config.logging.console).to.equal("debug");
     });
     it("should merge logging.files correctly", () => {
         const config = new DiscordBridgeConfig();
-        config.ApplyConfig({
+        config.applyConfig({
             logging: {
                 console: "silent",
                 files: [
@@ -58,6 +81,6 @@ describe("DiscordBridgeConfig.ApplyConfig", () => {
                 ],
             },
         });
-        expect(config.logging.files[0].file, "./bacon.log");
+        expect(config.logging.files[0].file).to.equal("./bacon.log");
     });
 });
diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts
index b4a398b238f96cd243aa03d4d9749207b93eff2e..10df5fd801e4b366a2d6b818e33787af382daa7b 100644
--- a/test/test_discordbot.ts
+++ b/test/test_discordbot.ts
@@ -25,6 +25,8 @@ import { MockMessage } from "./mocks/message";
 import { Util } from "../src/util";
 import { AppserviceMock } from "./mocks/appservicemock";
 import { MockUser } from "./mocks/user";
+import { MockChannel } from "./mocks/channel";
+import { DiscordBot } from "../src/bot";
 
 // we are a test file and thus need those
 /* tslint:disable:no-unused-expression max-file-line-count no-any */
@@ -43,11 +45,11 @@ const modDiscordBot = Proxyquire("../src/bot", {
             AsyncForEach: Util.AsyncForEach,
             DelayedPromise: Util.DelayedPromise,
             DownloadFile: async () => {
-                return Buffer.alloc(1024);
+                return Buffer.alloc(1000);
             },
             UploadContentFromUrl: async () => {
                 return {mxcUrl: "uploaded"};
-            }
+            },
         },
     },
 });
@@ -181,10 +183,10 @@ describe("DiscordBot", () => {
                 body: "someimage.png",
                 external_url: "asdf",
                 info: {
+                    h: 0,
                     mimetype: "image/png",
                     size: 42,
                     w: 0,
-                    h: 0,
                 },
                 msgtype: "m.image",
                 url: "mxc://someimage.png",
@@ -206,10 +208,10 @@ describe("DiscordBot", () => {
                 body: "foxes.mov",
                 external_url: "asdf",
                 info: {
+                    h: 0,
                     mimetype: "video/quicktime",
                     size: 42,
                     w: 0,
-                    h: 0,
                 },
                 msgtype: "m.video",
                 url: "mxc://foxes.mov",
@@ -255,7 +257,7 @@ describe("DiscordBot", () => {
                 external_url: "asdf",
                 info: {
                     mimetype: "application/zip",
-                    size: 42
+                    size: 42,
                 },
                 msgtype: "m.file",
                 url: "mxc://meow.zip",
@@ -401,4 +403,46 @@ 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 f78d66abded7dacaa4451b546796459cf7776e64..d66dd949e2cbe76da2780a1853e5acd6775ee4b6 100644
--- a/test/test_discordmessageprocessor.ts
+++ b/test/test_discordmessageprocessor.ts
@@ -58,8 +58,8 @@ describe("DiscordMessageProcessor", () => {
             msg.embeds = [];
             msg.content = "Hello World!";
             const result = await processor.FormatMessage(msg);
-            Chai.assert(result.body, "Hello World!");
-            Chai.assert(result.formattedBody, "Hello World!");
+            Chai.assert.equal(result.body, "Hello World!");
+            Chai.assert.equal(result.formattedBody, "Hello World!");
         });
         it("processes markdown messages correctly.", async () => {
             const processor = new DiscordMessageProcessor(
diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts
index 4e9c8a7605303f8d651096e6689f398ebe3b1d78..03ba47112152cd1d654e5b66e2d071893d10d81f 100644
--- a/test/test_matrixeventprocessor.ts
+++ b/test/test_matrixeventprocessor.ts
@@ -636,6 +636,7 @@ describe("MatrixEventProcessor", () => {
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
@@ -657,6 +658,7 @@ This is where the reply goes`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
@@ -678,6 +680,7 @@ This is where the reply goes`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
@@ -699,6 +702,7 @@ This is the second reply`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
@@ -720,6 +724,7 @@ This is the reply`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
@@ -739,10 +744,12 @@ This is the reply`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
-            expect(result!.timestamp!.getTime()).to.be.equal(TEST_TIMESTAMP);
+            // NOTE: Due to https://github.com/discordjs/discord.js/issues/3283, the typing is wrong here.
+            expect(result!.timestamp!).to.be.equal(TEST_TIMESTAMP);
         });
         it("should add field for discord replies", async () => {
             const processor = createMatrixEventProcessor();
@@ -755,6 +762,7 @@ This is the reply`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
@@ -779,6 +787,7 @@ This is the reply`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
@@ -796,6 +805,7 @@ This is the reply`,
                         },
                     },
                 },
+                room_id: "!fakeroom:localhost",
                 sender: "@test:localhost",
                 type: "m.room.message",
             } as IMatrixEvent, mockChannel as any);
diff --git a/tools/toolshelper.ts b/tools/toolshelper.ts
index 1d55842de19d4cb9d492d87152e49e7a40f18866..5e4dd35e62bb6b9d49ef34bbac58558c49894808 100644
--- a/tools/toolshelper.ts
+++ b/tools/toolshelper.ts
@@ -13,7 +13,7 @@ export class ToolsHelper {
     } {
         const registration = yaml.safeLoad(fs.readFileSync(regFile, "utf8"));
         const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(configFile, "utf8")) as DiscordBridgeConfig;
-
+        config.applyEnvironmentOverrides(process.env);
         if (registration === null) {
             throw Error("Failed to parse registration file");
         }
@@ -33,4 +33,4 @@ export class ToolsHelper {
             store,
         };
     }
-}
\ No newline at end of file
+}
diff --git a/tsconfig.json b/tsconfig.json
index b56c806b3b13595d99985097b41625102bdbf7fe..0513a4541aea9a8adcccf2bae90bd62895acad66 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,8 @@
         "inlineSourceMap": true,
         "outDir": "./build",
         "types": ["mocha", "node"],
-        "strictNullChecks": true
+        "strictNullChecks": true,
+        "allowSyntheticDefaultImports": true
     },
     "compileOnSave": true,
     "include": [
diff --git a/tslint.json b/tslint.json
index cd7de561c3b3ba02450df7cb5bf011afd9c73be2..ff80b039be7c4830651871937b672e77c31faca3 100644
--- a/tslint.json
+++ b/tslint.json
@@ -9,7 +9,7 @@
     "object-literal-sort-keys": "off",
     "no-any": true,
     "arrow-return-shorthand": true,
-    "no-magic-numbers": true,
+    "no-magic-numbers": [true, -1, 0, 1, 1000],
     "prefer-for-of": true,
     "typedef": {
       "severity": "warning"