diff --git a/README.md b/README.md
index 758b827cbbd030f9dd01324a8541ce23a396fde1..b0f3a223948f73e57902afff81b01c551093c72d 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,13 @@ Please also be aware that this is an unoffical project worked on in my (Half-Sho
 * Run ``node build/src/discordas.js -r -u "http://localhost:9005/" -c config.yaml``
 * Modify your HSs appservices config so that it includes the generated file.
 
+#### 3PID Protocol Support
+
+This bridge support searching for rooms within networks via the 3pid system
+used in clients like [Riot](https://riot.im). However, it requires a small manual change
+to your registration file. Add ``protocols: ["discord"]`` to the end and restart both your bridge
+and synapse. Any new servers/guilds you bridge should show up in the network list on Riot and other clients.
+
 ### Setting up Discord
 
 * Create a new application via https://discordapp.com/developers/applications/me/create
@@ -43,9 +50,6 @@ Please also be aware that this is an unoffical project worked on in my (Half-Sho
 In a vague order of what is coming up next
 
  - [x] Group messages
- - [ ] Direct messages
-  - [ ] Recieving
-  - [ ] Initiating
  - Matrix -> Discord
    - [x] Text content
    - [x] Image content
@@ -63,7 +67,9 @@ In a vague order of what is coming up next
   - [x] Rooms
   - [ ] Users
  - [ ] Puppet a user's real Discord account.
- - [ ] Rooms react to Discord updates
+  - [x] Sending messages
+  - [ ] Direct messages
+ - [x] Rooms react to Discord updates
  - [ ] Integrate Discord into existing rooms.
  - [ ] Manage channel from Matrix
   - [ ] Authorise admin rights from Discord to Matrix users
diff --git a/config/config.sample.yaml b/config/config.sample.yaml
index 9afb53742c488a905233dac05f4ca4cb71bfbf0b..af22a1841b458a7572d2e6b7d9167160825b15eb 100644
--- a/config/config.sample.yaml
+++ b/config/config.sample.yaml
@@ -5,3 +5,5 @@ auth:
   clientID: "12345" # Get from discord
   secret: "blah"
   botToken: "foobar"
+logging:
+  level: "warn" #silly, verbose, info, http, warn, error
diff --git a/config/config.schema.yaml b/config/config.schema.yaml
index 798771b14f4ebc5eb57f82021970728d0055f2eb..14dbddd7230c4bc3cadc03889d19f6a44b200aed 100644
--- a/config/config.schema.yaml
+++ b/config/config.schema.yaml
@@ -20,3 +20,9 @@ properties:
             type: "string"
           botToken:
             type: "string"
+    logging:
+        type: "object"
+        required: ["level"]
+        properties:
+          level:
+            type: "string"
diff --git a/docs/puppeting.md b/docs/puppeting.md
new file mode 100644
index 0000000000000000000000000000000000000000..4ca9062b9c5d525ba614a5d3575fbecc005beb3b
--- /dev/null
+++ b/docs/puppeting.md
@@ -0,0 +1,50 @@
+# Puppeting
+
+This docs describes the method to puppet yourself with the bridge, so you can
+interact with the bridge as if you were using the real Discord client. This
+has the benefits of (not all of these may be implemented):
+ * Talking as yourself, rather than as the bot.
+ * DM channels
+ * Able to use your Discord permissions, as well as joining rooms limited to
+   your roles as on Discord.
+
+## Caveats & Disclaimer
+
+Discord is currently __not__ offering any way to authenticate on behalf
+of a user _and_ interact on their behalf. The OAuth system does not allow
+remote access beyond reading information about the users. While [developers have
+expressed a wish for this](https://feedback.discordapp.com/forums/326712-discord-dream-land/suggestions/16753837-support-custom-clients)
+,it is my opinion that Discord are unlikely to support this any time soon. With
+all this said, Discord will not be banning users or the bridge itself for acting
+on the behalf of the user.
+
+Therefore while I loathe to do it, we have to store login tokens for *full
+permissions* on the user's account (excluding things such as changing passwords
+  and e-mail which require re-authenication, thankfully).
+
+The tokens will be stored by the bridge and are valid until the user
+changes their password, so please be careful not to give the token to anything
+that you wouldn't trust with your password.
+
+I accept no responsibility if Discord ban your IP, Account or even your details on
+their system. They have never given official support on custom clients (and
+  by extension, puppeting bridges). If you are in any doubt, stick to the
+  bot which is within the rules.
+
+## How to Puppet an Account
+*2FA does not work with bridging, please do not try it.*
+
+* Go to [Discord](https://discordapp.com/channels/@me) on your *browser* and log
+  in if you haven't.
+* Open the developer console (On Firefox/Chrome this is Shift+Control+C)
+* Click Storage or Application if it is not already selected.
+* On the left hand side there will be an option for "Local Storage", find this
+  and expand it and then click on the Discord option.
+* Find the option for token on the right hand side and copy the value, excluding
+  the `"`s
+* ~~Start a conversation with ``@_discord_bot:yourdomain`` on Matrix and send
+  the message "account.link Your_Token"~~
+* ~~The bridge should reply once it's managed to log you in.~~
+* Bot control has not been implemented yet, for now you will need to edit the
+  database and fill in user_tokens with your userId and token.
+* Congratulations, you are now puppeted.
diff --git a/package.json b/package.json
index 5566300968bf18cd7cef48723e7aad801484f95e..59e7cfa3ab2b5a80256a8a86afa470e1ebbb6daf 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,8 @@
     "coverage": "istanbul --include-all-sources cover -x build/src/discordas.js _mocha -- build/test/ -R spec",
     "build": "tsc",
     "start": "npm run-script build && node ./build/src/discordas.js -p 9005 -c config.yaml",
-    "getbotlink": "node ./tools/addbot.js"
+    "getbotlink": "node ./build/tools/addbot.js",
+    "adminme": "node ./build/tools/adminme.js"
   },
   "repository": {
     "type": "git",
@@ -29,14 +30,19 @@
   },
   "homepage": "https://github.com/Half-Shot/matrix-appservice-discord#readme",
   "dependencies": {
+    "@types/bluebird": "^3.0.37",
     "@types/node": "^7.0.5",
+    "@types/sqlite3": "^2.2.32",
     "bluebird": "^3.4.7",
+    "command-line-args": "^4.0.1",
+    "command-line-usage": "^4.0.0",
     "discord.js": "^11.0.0",
     "js-yaml": "^3.8.1",
     "marked": "^0.3.6",
     "matrix-appservice-bridge": "^1.3.5",
     "mime": "^1.3.4",
     "npmlog": "^4.0.2",
+    "sqlite3": "^3.1.8",
     "tslint": "^4.4.2",
     "typescript": "^2.1.6"
   },
diff --git a/src/bot.ts b/src/bot.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7749cbfc2821b2e515c24cc0199e1f9739f65c49
--- /dev/null
+++ b/src/bot.ts
@@ -0,0 +1,393 @@
+import { DiscordBridgeConfig } from "./config";
+import { DiscordClientFactory } from "./clientfactory";
+import { DiscordStore } from "./store";
+import { DiscordDMHandler } from "./dmhandler";
+import { MatrixUser, RemoteUser, Bridge, RemoteRoom, Entry } from "matrix-appservice-bridge";
+import { Util } from "./util";
+import * as Discord from "discord.js";
+import * as log from "npmlog";
+import * as Bluebird from "bluebird";
+import * as mime from "mime";
+import * as marked from "marked";
+import * as path from "path";
+
+// Due to messages often arriving before we get a response from the send call,
+// messages get delayed from discord.
+const MSG_PROCESS_DELAY = 750;
+const MATRIX_TO_LINK = "https://matrix.to/#/";
+
+class ChannelLookupResult {
+  public channel: Discord.TextChannel;
+  public botUser: boolean;
+}
+
+export class DiscordBot {
+  private config: DiscordBridgeConfig;
+  private clientFactory: DiscordClientFactory;
+  private store: DiscordStore;
+  private bot: Discord.Client;
+  private discordUser: Discord.ClientUser;
+  private bridge: Bridge;
+  private sentMessages: string[];
+  constructor(config: DiscordBridgeConfig, store: DiscordStore) {
+    this.config = config;
+    this.store = store;
+    this.sentMessages = [];
+    this.clientFactory = new DiscordClientFactory(config.auth, store);
+  }
+
+  public setBridge(bridge: Bridge) {
+    this.bridge = bridge;
+  }
+
+  public run (): Promise<null> {
+    return this.clientFactory.init().then(() => {
+      return this.clientFactory.getClient();
+    }).then((client: any) => {
+      client.on("typingStart", (c, u) => { this.OnTyping(c, u, true); });
+      client.on("typingStop", (c, u) => { this.OnTyping(c, u, false); });
+      client.on("userUpdate", (_, newUser) => { this.UpdateUser(newUser); });
+      client.on("channelUpdate", (_, newChannel) => { this.UpdateRooms(<Discord.TextChannel> newChannel); });
+      client.on("presenceUpdate", (_, newMember) => { this.UpdatePresence(newMember); });
+      client.on("message", (msg) => { Bluebird.delay(MSG_PROCESS_DELAY).then(() => {
+          this.OnMessage(msg);
+        });
+      });
+      this.bot = client;
+      return null;
+    });
+  }
+
+  public GetGuilds(): Discord.Guild[] {
+    return this.bot.guilds.array();
+  }
+
+  public ThirdpartySearchForChannels(guildId: string, channelName: string): any[] {
+    if (channelName.startsWith("#")) {
+      channelName = channelName.substr(1);
+    }
+    if (this.bot.guilds.has(guildId) ) {
+      const guild = this.bot.guilds.get(guildId);
+      return guild.channels.filter((channel) => {
+        return channel.name.toLowerCase() === channelName.toLowerCase(); // Implement searching in the future.
+      }).map((channel) => {
+        return {
+          alias: `#_discord_${guild.id}_${channel.id}:${this.config.bridge.domain}`,
+          protocol: "discord",
+          fields: {
+            guild_id: guild.id,
+            channel_name: channel.name,
+            channel_id: channel.id,
+          },
+        };
+      });
+    } else {
+      log.warn("DiscordBot", "Tried to do a third party lookup for a channel, but the guild did not exist");
+      return [];
+    }
+  }
+
+  public LookupRoom (server: string, room: string, sender?: string): Promise<ChannelLookupResult> {
+    let hasSender = sender !== null;
+    return this.clientFactory.getClient(sender).then((client) => {
+      const guild = client.guilds.get(server);
+      if (!guild) {
+        return Promise.reject(`Guild "${server}" not found`);
+      }
+      const channel = guild.channels.get(room);
+      if (channel) {
+        const lookupResult = new ChannelLookupResult();
+        lookupResult.channel = channel;
+        lookupResult.botUser = this.bot.user.id === client.user.id;
+        return lookupResult;
+      }
+      return Promise.reject(`Channel "${room}" not found`);
+    }).catch((err) => {
+      log.verbose("DiscordBot", "LookupRoom => ", err);
+      if (hasSender) {
+        log.verbose("DiscordBot", `Couldn't find guild/channel under user account. Falling back.`);
+        return this.LookupRoom(server, room, null);
+      }
+      throw err;
+    });
+  }
+
+  public ProcessMatrixMsgEvent(event, guildId: string, channelId: string): Promise<any> {
+    let chan;
+    let embed;
+    let botUser;
+    const mxClient = this.bridge.getClientFactory().getClientAs();
+    log.verbose("DiscordBot", `Looking up ${guildId}_${channelId}`);
+    return this.LookupRoom(guildId, channelId, event.sender).then((result) => {
+      log.verbose("DiscordBot", `Found channel! Looking up ${event.sender}`);
+      chan = result.channel;
+      botUser = result.botUser;
+      log.verbose("DiscordBot", botUser);
+      if (result.botUser) {
+        return mxClient.getProfileInfo(event.sender);
+      }
+      return null;
+    }).then((profile) => {
+      if (botUser === true) {
+        if (!profile.displayname) {
+          profile.displayname = event.sender;
+        }
+        if (profile.avatar_url) {
+          profile.avatar_url = mxClient.mxcUrlToHttp(profile.avatar_url);
+        }
+        embed = new Discord.RichEmbed({
+          author: {
+            name: profile.displayname,
+            icon_url: profile.avatar_url,
+            url: `https://matrix.to/#/${event.sender}`,
+            // TODO: Avatar
+          },
+          description: event.content.body,
+        });
+      }
+      if (["m.image", "m.audio", "m.video", "m.file"].indexOf(event.content.msgtype) !== -1) {
+        return Util.DownloadFile(mxClient.mxcUrlToHttp(event.content.url));
+      }
+      return Promise.resolve(null);
+    }).then((attachment) => {
+      if (attachment !== null) {
+        let name = this.GetFilenameForMediaEvent(event.content);
+        return {
+          file : {
+            name,
+            attachment,
+          },
+        };
+      }
+      return {};
+    }).then((opts) => {
+      if (botUser) {
+        return chan.sendEmbed(embed, opts);
+      }
+      return chan.sendMessage(event.content.body, opts);
+    }).then((msg) => {
+      this.sentMessages.push(msg.id);
+    }).catch((err) => {
+      log.error("DiscordBot", "Couldn't send message. ", err);
+    });
+  }
+
+  public OnUserQuery (userId: string): any {
+    return false;
+  }
+
+  private GetFilenameForMediaEvent(content) {
+    if (content.body) {
+      if (path.extname(content.body) !== "") {
+        return content.body;
+      }
+      return path.basename(content.body) + "." + mime.extension(content.info.mimetype);
+    }
+    return "matrix-media." + mime.extension(content.info.mimetype);
+  }
+
+  private GetRoomIdsFromChannel(channel: Discord.Channel): Promise<string[]> {
+    return this.bridge.getRoomStore().getEntriesByRemoteRoomData({
+      discord_channel: channel.id,
+    }).then((rooms) => {
+      if (rooms.length === 0) {
+        log.verbose("DiscordBot", `Got message but couldn"t find room chan id:${channel.id} for it.`);
+        return Promise.reject("Room not found.");
+      }
+      return rooms.map((room) => {return room.matrix.getId(); });
+    });
+  }
+
+  private UpdateRooms(discordChannel: Discord.TextChannel): Promise<null> {
+    const intent = this.bridge.getIntent();
+    const roomStore = this.bridge.getRoomStore();
+    return this.GetRoomIdsFromChannel(discordChannel).then((rooms) => {
+      return roomStore.getEntriesByMatrixIds(rooms).then( (entries) => {
+        return Object.keys(entries).map((key) => entries[key]);
+      });
+    }).then((entries: any) => {
+      return Promise.all(entries.map((entry) => {
+        if (entry.length === 0) {
+          return Promise.reject("Couldn't update room for channel, no assoicated entry in roomstore.");
+        }
+        return this.UpdateRoomEntry(entry[0], discordChannel);
+      }));
+    });
+  }
+
+  private UpdateRoomEntry(entry: Entry, discordChannel: Discord.TextChannel): Promise<null> {
+    const intent = this.bridge.getIntent();
+    const roomStore = this.bridge.getRoomStore();
+    const roomId = entry.matrix.getId();
+    return new Promise(() => {
+      const name = `[Discord] ${discordChannel.guild.name} #${discordChannel.name}`;
+      if (entry.remote.get("update_name") && entry.remote.get("discord_name") !== name) {
+        return intent.setRoomName(roomId, name).then(() => {
+          log.info("DiscordBot", `Updated name for ${roomId}`);
+          entry.remote.set("discord_name", name);
+          return roomStore.upsertEntry(entry);
+        });
+      }
+    }).then(() => {
+      if ( entry.remote.get("update_topic") && entry.remote.get("discord_topic") !== discordChannel.topic) {
+        return intent.setRoomTopic(roomId, discordChannel.topic).then(() => {
+          entry.remote.set("discord_topic", discordChannel.topic);
+          log.info("DiscordBot", `Updated topic for ${roomId}`);
+          return roomStore.upsertEntry(entry);
+        });
+      }
+    });
+  }
+
+  private UpdateUser(discordUser: Discord.User) {
+    let remoteUser: RemoteUser;
+    const displayName = discordUser.username + "#" + discordUser.discriminator;
+    const id = `_discord_${discordUser.id}:${this.config.bridge.domain}`;
+    const intent = this.bridge.getIntent("@" + id);
+    const userStore = this.bridge.getUserStore();
+
+    return userStore.getRemoteUser(discordUser.id).then((u) => {
+      remoteUser = u;
+      if (remoteUser === null) {
+        remoteUser = new RemoteUser(discordUser.id);
+        return userStore.linkUsers(
+          new MatrixUser(id),
+          remoteUser,
+        );
+      }
+      return Promise.resolve();
+    }).then(() => {
+      if (remoteUser.get("displayname") !== displayName) {
+        return intent.setDisplayName(displayName).then(() => {
+          remoteUser.set("displayname", displayName);
+          return userStore.setRemoteUser(remoteUser);
+        });
+      }
+      return true;
+    }).then(() => {
+      if (remoteUser.get("avatarurl") !== discordUser.avatarURL && discordUser.avatarURL !== null) {
+        return Util.UploadContentFromUrl(
+          this.bridge,
+          discordUser.avatarURL,
+          intent,
+          discordUser.avatar,
+        ).then((avatar) => {
+          intent.setAvatarUrl(avatar.mxc_url).then(() => {
+            remoteUser.set("avatarurl", discordUser.avatarURL);
+            return userStore.setRemoteUser(remoteUser);
+          });
+        });
+      }
+      return true;
+    });
+  }
+
+  private UpdatePresence(guildMember: Discord.GuildMember) {
+    log.info("DiscordBot", `Updating presence for ${guildMember.user.username}#${guildMember.user.discriminator}`);
+    const intent = this.bridge.getIntentFromLocalpart(`_discord_${guildMember.id}`);
+    try {
+      let presence = guildMember.presence.status;
+      if (presence === "idle" || presence === "dnd") {
+        presence = "unavailable";
+      }
+      intent.getClient().setPresence({
+        presence,
+      });
+    } catch (err) {
+      log.info("DiscordBot", "Couldn't set presence ", err);
+    }
+    // TODO: Set nicknames inside the scope of guild chats.
+  }
+
+  private OnTyping(channel: Discord.Channel, user: Discord.User, isTyping: boolean) {
+    return this.GetRoomIdsFromChannel(channel).then((rooms) => {
+      const intent = this.bridge.getIntentFromLocalpart(`_discord_${user.id}`);
+      return Promise.all(rooms.map((room) => {
+        return intent.sendTyping(room, isTyping);
+      }));
+    }).catch((err) => {
+      log.verbose("DiscordBot", "Failed to send typing indicator.", err);
+    });
+  }
+
+  private FormatDiscordMessage(msg: Discord.Message): string {
+    // Replace Users
+    let content = msg.content;
+    const userRegex = /<@!?([0-9]*)>/g;
+    let results = userRegex.exec(content);
+    while (results !== null) {
+      const id = results[1];
+      const member = msg.guild.members.get(id);
+      let memberId = `@_discord_${id}:${this.config.bridge.domain}`;
+      let memberStr = member ? member.user.username : memberId;
+      content = content.replace(results[0], memberStr);
+      results = userRegex.exec(content);
+    }
+    // Replace channels
+    const channelRegex = /<#?([0-9]*)>/g;
+    results = channelRegex.exec(content);
+    while (results !== null) {
+      const id = results[1];
+      const channel = msg.guild.channels.get(id);
+      let roomId = `#_discord_${msg.guild.id}_${id}:${this.config.bridge.domain}`;
+      let channelStr = channel ? "#" + channel.name : "#" + id;
+      content = content.replace(results[0], `[${channelStr}](${MATRIX_TO_LINK}${roomId})`);
+      results = channelRegex.exec(content);
+    }
+    return content;
+  }
+
+  private OnMessage(msg: Discord.Message) {
+    const indexOfMsg = this.sentMessages.indexOf(msg.id);
+    if (indexOfMsg !== -1) {
+      log.verbose("DiscordBot", "Got repeated message, ignoring.");
+      delete this.sentMessages[indexOfMsg];
+      return; // Skip *our* messages
+    }
+    this.UpdateUser(msg.author).then(() => {
+      return this.GetRoomIdsFromChannel(msg.channel);
+    }).then((rooms) => {
+      const intent = this.bridge.getIntentFromLocalpart(`_discord_${msg.author.id}`);
+      // Check Attachements
+      msg.attachments.forEach((attachment) => {
+        Util.UploadContentFromUrl(this.bridge, attachment.url, intent, attachment.filename).then((content) => {
+          const fileMime = mime.lookup(attachment.filename);
+          const msgtype = attachment.height ? "m.image" : "m.file";
+          const info = {
+            mimetype: fileMime,
+            size: attachment.filesize,
+            w: null,
+            h: null,
+          };
+          if (msgtype === "m.image") {
+            info.w = attachment.width;
+            info.h = attachment.height;
+          }
+          rooms.forEach((room) => {
+            intent.sendMessage(room, {
+              body: attachment.filename,
+              info,
+              msgtype,
+              url: content.mxc_url,
+            });
+          });
+        });
+      });
+      if (msg.content !== null && msg.content !== "") {
+        // Replace mentions.
+        let content = this.FormatDiscordMessage(msg);
+        const fBody = marked(content);
+        rooms.forEach((room) => {
+          intent.sendMessage(room, {
+            body: content,
+            msgtype: "m.text",
+            formatted_body: fBody,
+            format: "org.matrix.custom.html",
+          });
+        });
+      }
+    }).catch((err) => {
+      log.warn("DiscordBot", "Failed to send message into room.", err);
+    });
+  }
+}
diff --git a/src/clientfactory.ts b/src/clientfactory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f8e9958c7aeda30ed73254ff1f50107f6eed4100
--- /dev/null
+++ b/src/clientfactory.ts
@@ -0,0 +1,62 @@
+import { DiscordBridgeConfigAuth } from "./config";
+import { DiscordStore } from "./store";
+import { Client } from "discord.js";
+import * as log from "npmlog";
+import * as Bluebird from "bluebird";
+
+export class DiscordClientFactory {
+  private config: DiscordBridgeConfigAuth;
+  private store: DiscordStore;
+  private botClient: any;
+  private clients: Map<string, any>;
+  constructor(config: DiscordBridgeConfigAuth, store: DiscordStore) {
+    this.config = config;
+    this.clients = new Map();
+    this.store = store;
+  }
+
+  public init(): Promise<null> {
+    // We just need to make sure we have a bearer token.
+    // Create a new Bot client.
+    this.botClient = Bluebird.promisifyAll(new Client({
+      fetchAllMembers: true,
+      sync: true,
+      messageCacheLifetime: 5,
+    }));
+    return this.botClient.login(this.config.botToken).then(() => {
+      return null; // Strip token from promise.
+    }).catch((err) => {
+      log.error("ClientFactory", "Could not login as the bot user. This is bad!");
+    });
+  }
+
+  public getClient(userId?: string): Promise<any> {
+    let client;
+    if (userId) {
+      if (this.clients.has(userId)) {
+        log.verbose("ClientFactory", "Returning cached user client.");
+        return Promise.resolve(this.clients.get(userId));
+      }
+      return this.store.get_user_token(userId).then((token) => {
+        if (token === null) {
+          return Promise.resolve(this.botClient);
+        }
+        client = Bluebird.promisifyAll(new Client({
+          fetchAllMembers: true,
+          sync: true,
+          messageCacheLifetime: 5,
+        }));
+        log.verbose("ClientFactory", "Got user token. Logging in...");
+        return client.login(token).then(() => {
+          log.verbose("ClientFactory", "Logged in. Storing ", userId);
+          this.clients.set(userId, client);
+          return Promise.resolve(client);
+        }).catch((err) => {
+          log.warn("ClientFactory", `Could not log ${userId} in.`, err);
+        });
+      });
+      // Get from cache
+    }
+    return Promise.resolve(this.botClient);
+  }
+}
diff --git a/src/config.ts b/src/config.ts
index d2c7779424f11a5d3296eedbc0b8858fc3b3387c..d5b266c822e99a5f8e5b372275e642ad3c04e748 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -3,7 +3,7 @@
 export class DiscordBridgeConfig {
   public bridge: DiscordBridgeConfigBridge;
   public auth: DiscordBridgeConfigAuth;
-  public guilds: DiscordBridgeConfigGuilds[];
+  public logging: DiscordBridgeConfigLogging;
 }
 
 class DiscordBridgeConfigBridge {
@@ -11,13 +11,11 @@ class DiscordBridgeConfigBridge {
   public homeserverUrl: string;
 }
 
-class DiscordBridgeConfigAuth {
+export class DiscordBridgeConfigAuth {
   public clientID: string;
   public secret: string;
   public botToken: string;
 }
-
-class DiscordBridgeConfigGuilds {
-  public id: string;
-  public aliasName: string;
+class DiscordBridgeConfigLogging {
+  public level: string;
 }
diff --git a/src/dbschema/dbschema.ts b/src/dbschema/dbschema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..655b75906fc4c8b2c6cd77aa0c693e43eaa35f49
--- /dev/null
+++ b/src/dbschema/dbschema.ts
@@ -0,0 +1,5 @@
+import { DiscordStore } from "../store";
+export interface IDbSchema {
+  description: string;
+  run(store: DiscordStore): Promise<null>;
+}
diff --git a/src/dbschema/v1.ts b/src/dbschema/v1.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ca542623e9df7ec6ae46599c4eaf5e4e2be8c3e4
--- /dev/null
+++ b/src/dbschema/v1.ts
@@ -0,0 +1,19 @@
+import {IDbSchema} from "./dbschema";
+import {DiscordStore} from "../store";
+export class Schema implements IDbSchema {
+  public description = "Schema, Client Auth Table";
+  public run(store: DiscordStore): Promise<null> {
+    return store.create_table(`
+    CREATE TABLE schema (
+      version	INTEGER UNIQUE NOT NULL
+    );`, "schema").then(() => {
+      return store.db.runAsync("INSERT INTO schema VALUES (0);");
+    }).then(() => {
+      return store.create_table(`
+      CREATE TABLE user_tokens (
+        userId TEXT UNIQUE NOT NULL,
+        token TEXT UNIQUE NOT NULL
+      );`, "user_tokens");
+    })
+  }
+}
diff --git a/src/discordas.ts b/src/discordas.ts
index 9a2fab766f336c77d7916695e5c4e962ff191f91..c8d6eb6262847b4df5368be32e00b0457a424281 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -3,8 +3,9 @@ import * as log from "npmlog";
 import * as yaml from "js-yaml";
 import * as fs from "fs";
 import { DiscordBridgeConfig } from "./config";
-import { DiscordBot } from "./discordbot";
+import { DiscordBot } from "./bot";
 import { MatrixRoomHandler } from "./matrixroomhandler";
+import { DiscordStore } from "./store";
 
 const cli = new Cli({
   bridgeConfig: {
@@ -34,6 +35,7 @@ function generateRegistration(reg, callback)  {
 }
 
 function run (port: number, config: DiscordBridgeConfig) {
+  log.level = config.logging ? (config.logging.level || "warn") : "warn";
   log.info("discordas", "Starting Discord AS");
   const yamlConfig = yaml.safeLoad(fs.readFileSync("discord-registration.yaml", "utf8"));
   const registration = AppServiceRegistration.fromObject(yamlConfig);
@@ -46,26 +48,21 @@ function run (port: number, config: DiscordBridgeConfig) {
     token: registration.as_token,
     url: config.bridge.homeserverUrl,
   });
-  const discordbot = new DiscordBot(config);
+  const discordstore = new DiscordStore("discord.db");
+  const discordbot = new DiscordBot(config, discordstore);
   const roomhandler = new MatrixRoomHandler(discordbot, config, botUserId);
 
   const bridge = new Bridge({
     clientFactory,
     controller: {
       // onUserQuery: userQuery,
-      onAliasQuery: (alias, aliasLocalpart) => {
-        return roomhandler.OnAliasQuery(alias, aliasLocalpart);
-      },
+      onAliasQuery: roomhandler.OnAliasQuery.bind(roomhandler),
       onEvent: roomhandler.OnEvent.bind(roomhandler),
       onAliasQueried: roomhandler.OnAliasQueried.bind(roomhandler),
       thirdPartyLookup: roomhandler.ThirdPartyLookup,
-      // onLog: function (line, isError) {
-      //   if(isError) {
-      //     if(line.indexOf("M_USER_IN_USE") === -1) {//QUIET!
-      //       log.warn("matrix-appservice-bridge", line);
-      //     }
-      //   }
-      // }
+      onLog: (line, isError) => {
+        log.verbose("matrix-appservice-bridge", line);
+      },
     },
     domain: config.bridge.domain,
     homeserverUrl: config.bridge.homeserverUrl,
@@ -73,9 +70,12 @@ function run (port: number, config: DiscordBridgeConfig) {
   });
   roomhandler.setBridge(bridge);
   discordbot.setBridge(bridge);
-
+  log.info("discordas", "Initing bridge.");
   log.info("AppServ", "Started listening on port %s at %s", port, new Date().toUTCString() );
   bridge.run(port, config);
-  discordbot.run();
-
+  log.info("discordas", "Initing store.");
+  discordstore.init().then(() => {
+    log.info("discordas", "Initing bot.");
+    return discordbot.run();
+  });
 }
diff --git a/src/discordbot.ts b/src/discordbot.ts
deleted file mode 100644
index 7d63d8c23c8f7603afeadda9865d0a01968a05a5..0000000000000000000000000000000000000000
--- a/src/discordbot.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-import { DiscordBridgeConfig } from "./config";
-import * as Discord from "discord.js";
-import * as log from "npmlog";
-import { MatrixUser, RemoteUser, Bridge, RemoteRoom } from "matrix-appservice-bridge";
-import { Util } from "./util";
-import * as Bluebird from "bluebird";
-import * as mime from "mime";
-import * as marked from "marked";
-
-export class DiscordBot {
-  private config: DiscordBridgeConfig;
-  private bot: Discord.Client;
-  private discordUser: Discord.ClientUser;
-  private bridge: Bridge;
-  constructor(config: DiscordBridgeConfig) {
-    this.config = config;
-  }
-
-  public setBridge(bridge: Bridge) {
-    this.bridge = bridge;
-  }
-
-  public run (): Promise<null> {
-    this.bot = Bluebird.promisifyAll(new Discord.Client());
-    this.bot.on("typingStart", (c, u) => { this.OnTyping(c, u, true); });
-    this.bot.on("typingStop", (c, u) => { this.OnTyping(c, u, false); });
-    this.bot.on("userUpdate", (_, newUser) => { this.UpdateUser(newUser); });
-    this.bot.on("channelUpdate", (_, newChannel) => { this.UpdateRoom(<Discord.TextChannel> newChannel); });
-    this.bot.on("presenceUpdate", (_, newMember) => { this.UpdatePresence(newMember); });
-    this.bot.on("message", this.OnMessage.bind(this));
-    const promise = (this.bot as any).onAsync("ready");
-    this.bot.login(this.config.auth.botToken);
-
-    return promise;
-  }
-
-  public GetBot (): Discord.Client {
-    return this.bot;
-  }
-
-  public GetGuilds(): Discord.Guild[] {
-    return this.bot.guilds.array();
-  }
-
-  public ThirdpartySearchForChannels(guildId: string, channelName: string): any[] {
-    if (channelName.startsWith("#")) {
-      channelName = channelName.substr(1);
-    }
-    if (this.bot.guilds.has(guildId) ) {
-      const guild = this.bot.guilds.get(guildId);
-      return guild.channels.filter((channel) => {
-        return channel.name.toLowerCase() === channelName.toLowerCase(); // Implement searching in the future.
-      }).map((channel) => {
-        return {
-          alias: `#_discord_${guild.id}_${channel.id}:${this.config.bridge.domain}`,
-          protocol: "discord",
-          fields: {
-            guild_id: guild.id,
-            channel_name: channel.name,
-            channel_id: channel.id,
-          },
-        };
-      });
-    } else {
-      log.warn("DiscordBot", "Tried to do a third party lookup for a channel, but the guild did not exist");
-      return [];
-    }
-  }
-
-  public LookupRoom (server: string, room: string): Promise<Discord.TextChannel> {
-    const guild = this.bot.guilds.find((g) => {
-      return (g.id === server);
-    });
-    if (!guild) {
-      return Promise.reject(`Guild "${server}" not found`);
-    }
-
-    const channel = guild.channels.find((c) => {
-      return (c.id === room);
-    });
-
-    if (!channel) {
-      return Promise.reject(`Channel "${room}" not found`);
-    }
-    return Promise.resolve(channel);
-  }
-
-  public ProcessMatrixMsgEvent(event, guildId: string, channelId: string): Promise<any> {
-    let chan;
-    let embed;
-    const mxClient = this.bridge.getClientFactory().getClientAs();
-    return this.LookupRoom(guildId, channelId).then((channel) => {
-      chan = channel;
-      return mxClient.getProfileInfo(event.sender);
-    }).then((profile) => {
-      if (!profile.displayname) {
-        profile.displayname = event.sender;
-      }
-      if (profile.avatar_url) {
-        profile.avatar_url = mxClient.mxcUrlToHttp(profile.avatar_url);
-      }
-      embed = new Discord.RichEmbed({
-        author: {
-          name: profile.displayname,
-          icon_url: profile.avatar_url,
-          url: `https://matrix.to/#/${event.sender}`,
-          // TODO: Avatar
-        },
-        description: event.content.body,
-      });
-      if (["m.image", "m.audio", "m.video", "m.file"].indexOf(event.content.msgtype) !== -1) {
-        return Util.DownloadFile(mxClient.mxcUrlToHttp(event.content.url));
-      }
-      return Promise.resolve(null);
-    }).then((attachment) => {
-      if (attachment !== null) {
-        return {
-          file : {
-            name: event.content.body,
-            attachment,
-          },
-        };
-      }
-      return {};
-    }).then((opts) => {
-      chan.sendEmbed(embed, opts);
-    }).catch((err) => {
-      log.error("DiscordBot", "Couldn't send message. ", err);
-    });
-  }
-
-  public OnUserQuery (userId: string): any {
-    return false;
-  }
-
-  private GetRoomIdFromChannel(channel: Discord.Channel): Promise<string> {
-    return this.bridge.getRoomStore().getEntriesByRemoteRoomData({
-      discord_channel: channel.id,
-    }).then((rooms) => {
-      if (rooms.length === 0) {
-        log.warn("DiscordBot", `Got message but couldn"t find room chan id:${channel.id} for it.`);
-        return Promise.reject("Room not found.");
-      }
-      return rooms[0].matrix.getId();
-    });
-  }
-
-  private UpdateRoom(discordChannel: Discord.TextChannel): Promise<null> {
-    const intent = this.bridge.getIntent();
-    const roomStore = this.bridge.getRoomStore();
-    let entry: RemoteRoom;
-    let roomId = null;
-    return this.GetRoomIdFromChannel(discordChannel).then((r) => {
-      roomId = r;
-      return roomStore.getEntriesByMatrixId(roomId);
-    }).then((entries) => {
-      if (entries.length === 0) {
-        return Promise.reject("Couldn't update room for channel, no assoicated entry in roomstore.");
-      }
-      entry = entries[0];
-      return;
-    }).then(() => {
-      const name = `[Discord] ${discordChannel.guild.name} #${discordChannel.name}`;
-      if (entry.remote.get("discord_name") !== name) {
-        return intent.setRoomName(roomId, name).then(() => {
-          entry.remote.set("discord_name", name);
-          return roomStore.upsertEntry(entry);
-        });
-      }
-    }).then(() => {
-      if (entry.remote.get("discord_topic") !== discordChannel.topic) {
-        return intent.setRoomTopic(roomId, discordChannel.topic).then(() => {
-          entry.remote.set("discord_topic", discordChannel.topic);
-          return roomStore.upsertEntry(entry);
-        });
-      }
-    });
-  }
-
-  private UpdateUser(discordUser: Discord.User) {
-    let remoteUser: RemoteUser;
-    const displayName = discordUser.username + "#" + discordUser.discriminator;
-    const id = `_discord_${discordUser.id}:${this.config.bridge.domain}`;
-    const intent = this.bridge.getIntent("@" + id);
-    const userStore = this.bridge.getUserStore();
-
-    return userStore.getRemoteUser(discordUser.id).then((u) => {
-      remoteUser = u;
-      if (remoteUser === null) {
-        remoteUser = new RemoteUser(discordUser.id);
-        return userStore.linkUsers(
-          new MatrixUser(id),
-          remoteUser,
-        );
-      }
-      return Promise.resolve();
-    }).then(() => {
-      if (remoteUser.get("displayname") !== displayName) {
-        return intent.setDisplayName(displayName).then(() => {
-          remoteUser.set("displayname", displayName);
-          return userStore.setRemoteUser(remoteUser);
-        });
-      }
-      return true;
-    }).then(() => {
-      if (remoteUser.get("avatarurl") !== discordUser.avatarURL && discordUser.avatarURL !== null) {
-        return Util.UploadContentFromUrl(
-          this.bridge,
-          discordUser.avatarURL,
-          intent,
-          discordUser.avatar,
-        ).then((avatar) => {
-          intent.setAvatarUrl(avatar.mxc_url).then(() => {
-            remoteUser.set("avatarurl", discordUser.avatarURL);
-            return userStore.setRemoteUser(remoteUser);
-          });
-        });
-      }
-      return true;
-    });
-  }
-
-  private UpdatePresence(guildMember: Discord.GuildMember) {
-    log.info("DiscordBot", `Updating presence for ${guildMember.user.username}#${guildMember.user.discriminator}`);
-    const intent = this.bridge.getIntentFromLocalpart(`_discord_${guildMember.id}`);
-    try {
-      let presence = guildMember.presence.status;
-      if (presence === "idle" || presence === "dnd") {
-        presence = "unavailable";
-      }
-      intent.getClient().setPresence({
-        presence,
-      });
-    } catch (err) {
-      log.info("DiscordBot", "Couldn't set presence ", err);
-    }
-    // TODO: Set nicknames inside the scope of guild chats.
-  }
-
-  private OnTyping(channel: Discord.Channel, user: Discord.User, isTyping: boolean) {
-    return this.GetRoomIdFromChannel(channel).then((room) => {
-      const intent = this.bridge.getIntentFromLocalpart(`_discord_${user.id}`);
-      intent.sendTyping(room, isTyping);
-    });
-  }
-
-  private OnMessage(msg: Discord.Message) {
-    if (msg.author.id === this.bot.user.id) {
-      return; // Skip *our* messages
-    }
-    this.UpdateUser(msg.author).then(() => {
-      return this.GetRoomIdFromChannel(msg.channel);
-    }).then((room) => {
-      const intent = this.bridge.getIntentFromLocalpart(`_discord_${msg.author.id}`);
-      // Check Attachements
-      msg.attachments.forEach((attachment) => {
-        Util.UploadContentFromUrl(this.bridge, attachment.url, intent, attachment.filename).then((content) => {
-          const fileMime = mime.lookup(attachment.filename);
-          const msgtype = attachment.height ? "m.image" : "m.file";
-          const info = {
-            mimetype: fileMime,
-            size: attachment.filesize,
-            w: null,
-            h: null,
-          };
-          if (msgtype === "m.image") {
-            info.w = attachment.width;
-            info.h = attachment.height;
-          }
-          intent.sendMessage(room, {
-            body: attachment.filename,
-            info,
-            msgtype,
-            url: content.mxc_url,
-          });
-        });
-      });
-      if (msg.content !== null && msg.content !== "") {
-        // Replace mentions.
-        const content = msg.content.replace(/<@[0-9]*>/g, (item) => {
-          const id = item.substr(2, item.length - 3);
-          const member = msg.guild.members.get(id);
-          if (member) {
-            return member.user.username;
-          } else {
-            return `@_discord_${id}:${this.config.bridge.domain}`;
-          }
-        });
-        intent.sendMessage(room, {
-          body: content,
-          msgtype: "m.text",
-          formatted_body: marked(content),
-          format: "org.matrix.custom.html",
-        });
-      }
-    });
-  }
-}
diff --git a/src/dmhandler.ts b/src/dmhandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9f0d4af6c7c683b12a706a4ae4a6502d0c76fbe0
--- /dev/null
+++ b/src/dmhandler.ts
@@ -0,0 +1,3 @@
+export class DiscordDMHandler {
+  
+}
diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts
index c2545348b59bb0ecc42fb823cb7afb242aa2fdb6..f4d647e3fc1a09262bb58acfeca7b6791bb14e89 100644
--- a/src/matrixroomhandler.ts
+++ b/src/matrixroomhandler.ts
@@ -1,4 +1,4 @@
-import { DiscordBot } from "./discordbot";
+import { DiscordBot } from "./bot";
 import {
   Bridge,
   RemoteRoom,
@@ -8,6 +8,7 @@ import {
   thirdPartyLocationResult,
  } from "matrix-appservice-bridge";
 import { DiscordBridgeConfig } from "./config";
+import { DiscordClientFactory } from "./clientfactory";
 
 import * as Discord from "discord.js";
 import * as log from "npmlog";
@@ -47,19 +48,24 @@ export class MatrixRoomHandler {
   public OnEvent (request, context) {
     const event = request.getData();
     if (event.type === "m.room.message" && context.rooms.remote) {
+      log.verbose("MatrixRoomHandler", "Got m.room.message event");
       let srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", 2);
       this.discord.ProcessMatrixMsgEvent(event, srvChanPair[0], srvChanPair[1]);
+    } else {
+      log.verbose("MatrixRoomHandler", "Got non m.room.message event");
     }
   }
 
   public OnAliasQuery (alias: string, aliasLocalpart: string): Promise<any> {
+    log.info("MatrixRoomHandler", "Got request for #", aliasLocalpart);
     let srvChanPair = aliasLocalpart.substr("_discord_".length).split("_", 2);
     if (srvChanPair.length < 2 || srvChanPair[0] === "" || srvChanPair[1] === "") {
       log.warn("MatrixRoomHandler", `Alias '${aliasLocalpart}' was missing a server and/or a channel`);
       return;
     }
-    return this.discord.LookupRoom(srvChanPair[0], srvChanPair[1]).then((channel) => {
-      return this.createMatrixRoom(channel, aliasLocalpart);
+    return this.discord.LookupRoom(srvChanPair[0], srvChanPair[1]).then((result) => {
+      log.info("MatrixRoomHandler", "Creating #", aliasLocalpart);
+      return this.createMatrixRoom(result.channel, aliasLocalpart);
     }).catch((err) => {
       log.error("MatrixRoomHandler", `Couldn't find discord room '${aliasLocalpart}'.`, err);
     });
@@ -112,7 +118,6 @@ export class MatrixRoomHandler {
   public tpGetLocation(protocol: string, fields: any): Promise<thirdPartyLocationResult[]> {
     log.info("MatrixRoomHandler", "Got location request ", protocol, fields);
     const chans = this.discord.ThirdpartySearchForChannels(fields.guild_id, fields.channel_name);
-    console.log(chans);
     return Promise.resolve(chans);
   }
 
@@ -140,6 +145,8 @@ export class MatrixRoomHandler {
     remote.set("discord_type", "text");
     remote.set("discord_guild", channel.guild.id);
     remote.set("discord_channel", channel.id);
+    remote.set("update_name", true);
+    remote.set("update_topic", true);
 
     const gname = channel.guild.name.replace(" ", "-");
     const cname = channel.name.replace(" ", "-");
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1c1799f9d0dc5161d87a3436fc1c6dde1e494bab
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,136 @@
+import * as SQLite3 from "sqlite3";
+import * as log from "npmlog";
+import * as Bluebird from "bluebird";
+import { IDbSchema } from "./dbschema/dbschema";
+
+const CURRENT_SCHEMA = 1;
+/**
+ * Stores data for specific users and data not specific to rooms.
+ */
+export class DiscordStore {
+  /**
+   * @param  {string} filepath Location of the SQLite database file.
+   */
+  public db: any;
+  private version: number;
+  constructor (filepath) {
+    this.db = new SQLite3.Database(filepath, (err) => {
+      if (err) {
+        log.error("DiscordStore", "Error opening database, %s");
+        throw new Error("Couldn't open database. The appservice won't be able to continue.");
+      }
+    });
+    this.db = Bluebird.promisifyAll(this.db);
+    this.version = null;
+  }
+
+  /**
+   * Checks the database has all the tables needed.
+   */
+  public init () {
+    log.info("DiscordStore", "Starting DB Init");
+    let oldVersion;
+    let version;
+    return this.getSchemaVersion().then( (v) => {
+      oldVersion = v;
+      version = v;
+      let promises = [];
+      while (version < CURRENT_SCHEMA) {
+        version++;
+        const schemaClass = require(`./dbschema/v${version}.js`).Schema;
+        const schema = (new schemaClass() as IDbSchema);
+        log.info("DiscordStore", `Updating database to v${version}, ${schema.description}`);
+        promises.push(schema.run(this).then(() => {
+          log.info("DiscordStore", "Updated database v%s", version);
+        }));
+        this.version = version;
+      }
+      return Promise.all(promises);
+    }).then( () => {
+      return this.setSchemaVersion(oldVersion, version).then( () => {
+        log.info("DiscordStore", "Updated database to the latest schema");
+      });
+    }).catch( (err) => {
+      log.error("DiscordStore", "Couldn't update database to the latest version! Bailing");
+      throw err;
+    });
+  }
+
+  public create_table (statement, tablename) {
+    return this.db.runAsync(statement).then(() => {
+      log.info("DiscordStore", "Created table ", tablename);
+    }).catch((err) => {
+      throw new Error(`Error creating '${tablename}': ${err}`);
+    });
+  }
+
+  public close () {
+    this.db.close();
+  }
+
+  public set_user_token(userId: string, token: string) {
+    log.silly("SQL", "set_user_token => %s", userId);
+    return this.db.runAsync(
+      `REPLACE INTO user_tokens (userId,token) VALUES ($id,$token);`
+    , {
+      $id: userId,
+      $token: token,
+    }).catch( (err) => {
+      log.error("TwitDB", "Error storing user token %s", err);
+      throw err;
+    });
+  }
+
+  public get_user_token(userId: string): Promise<string> {
+    log.silly("SQL", "get_user_token => %s", userId);
+    return this.db.getAsync(
+      `
+      SELECT token
+      FROM user_tokens
+      WHERE user_tokens.userId = $id;
+      `
+    , {
+      $id: userId,
+    }).then( (row) => {
+      return row !== undefined ? row.token : null;
+    }).catch( (err) => {
+      log.error("TwitDB", "Error getting user token  %s", err.Error);
+      throw err;
+    });
+  }
+
+  public get_users_tokens(): Promise<any> {
+    log.silly("SQL", "get_users_tokens");
+    return this.db.allAsync(
+      `
+      SELECT *
+      FROM user_tokens
+      `,
+    ).then( (rows) => {
+      return rows;
+    }).catch( (err) => {
+      log.error("TwitDB", "Error getting user token  %s", err.Error);
+      throw err;
+    });
+  }
+
+  private getSchemaVersion ( ) {
+    log.silly("DiscordStore", "_get_schema_version");
+    return this.db.getAsync(`SELECT version FROM schema`).then((row) => {
+      return row === undefined ? 0 : row.version;
+    }).catch( ()  => {
+      return 0;
+    });
+  }
+
+  private setSchemaVersion (oldVer: number, ver: number) {
+    log.silly("DiscordStore", "_set_schema_version => %s", ver);
+    return this.db.getAsync(
+      `
+      UPDATE schema
+      SET version = $ver
+      WHERE version = $old_ver
+      `, {$ver: ver, $old_ver: oldVer},
+    );
+  }
+}
diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts
index 40914bc440369da90e512ddfc5724de8a08bb781..46345db761962c43b64e094f134607584ec7883e 100644
--- a/test/test_discordbot.ts
+++ b/test/test_discordbot.ts
@@ -121,39 +121,15 @@ describe("DiscordBot", () => {
     );
     discordBot.run();
     it("should reject a missing guild.", () => {
-      return assert.isRejected(discordBot.LookupRoom("MyMissingGuild", "achannel"));
-    });
-
-    it("should resolve a guild.", () => {
-      return assert.isFulfilled(discordBot.LookupRoom("MyGuild", "achannel"));
-    });
-
-    it("should resolve a guild with an id.", () => {
-      return assert.isFulfilled(discordBot.LookupRoom("123", "achannel"));
-    });
-
-    it("should resolve a guild with spaces.", () => {
-      return assert.isFulfilled(discordBot.LookupRoom("My-Spaces-Guild", "achannel"));
-    });
-
-    it("should resolve a guild with dashes.", () => {
-      return assert.isFulfilled(discordBot.LookupRoom("My-Dash-Guild", "achannel"));
+      return assert.isRejected(discordBot.LookupRoom("541", "321"));
     });
 
     it("should reject a missing channel.", () => {
-      return assert.isRejected(discordBot.LookupRoom("MyGuild", "amissingchannel"));
-    });
-
-    it("should resolve a channel with spaces.", () => {
-      return assert.isFulfilled(discordBot.LookupRoom("MyGuild", "a channel"));
+      return assert.isRejected(discordBot.LookupRoom("123", "666"));
     });
 
-    it("should resolve a channel with dashes.", () => {
-      return assert.isFulfilled(discordBot.LookupRoom("MyGuild", "a-channel"));
-    });
-
-    it("should resolve a channel with an id.", () => {
-      return assert.isFulfilled(discordBot.LookupRoom("MyGuild", "321"));
+    it("should resolve a guild and channel id.", () => {
+      return assert.isFulfilled(discordBot.LookupRoom("123", "321"));
     });
   });
   // describe("ProcessMatrixMsgEvent()", () => {
@@ -171,8 +147,8 @@ describe("DiscordBot", () => {
   describe("OnTyping()", () => {
     const discordBot = new modDiscordBot.DiscordBot(
       config,
-      mockBridge,
     );
+    discordBot.setBridge(mockBridge);
     discordBot.run();
     it("should reject an unknown room.", () => {
       return assert.isRejected(discordBot.OnTyping( {id: "512"}, {id: "12345"}, true));
diff --git a/tools/addbot.js b/tools/addbot.js
deleted file mode 100644
index 86d55daca8199114718094ba074c18c8d06e1463..0000000000000000000000000000000000000000
--- a/tools/addbot.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const yaml = require("js-yaml");
-const fs = require("fs");
-const flags = require("../node_modules/discord.js/src/util/Constants.js").PermissionFlags;
-const yamlConfig = yaml.safeLoad(fs.readFileSync("config.yaml", "utf8"));
-if (yamlConfig === null) {
-  console.error("You have an error in your discord config.");
-}
-const client_id = yamlConfig.auth.clientID;
-const perms = flags.READ_MESSAGES |
-  flags.SEND_MESSAGES |
-  flags.CHANGE_NICKNAME |
-  flags.CONNECT |
-  flags.SPEAK |
-  flags.EMBED_LINKS |
-  flags.ATTACH_FILES |
-  flags.READ_MESSAGE_HISTORY;
-
-console.log(`Go to https://discordapp.com/api/oauth2/authorize?client_id=${client_id}&scope=bot&permissions=${perms} to invite the bot into a guild.`);
diff --git a/tools/addbot.ts b/tools/addbot.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f54d66beb66e6b5f707e1c6b0131257ce517ab05
--- /dev/null
+++ b/tools/addbot.ts
@@ -0,0 +1,25 @@
+ /* tslint:disable:no-bitwise no-console no-var-requires */
+/**
+ * Generates a URL you can use to authorize a bot with a guild.
+ */
+import * as yaml from "js-yaml";
+import * as fs from "fs";
+
+const flags = require("../../node_modules/discord.js/src/util/Constants.js").PermissionFlags;
+const yamlConfig = yaml.safeLoad(fs.readFileSync("config.yaml", "utf8"));
+if (yamlConfig === null) {
+  console.error("You have an error in your discord config.");
+}
+const clientId = yamlConfig.auth.clientID;
+
+const perms = flags.READ_MESSAGES |
+  flags.SEND_MESSAGES |
+  flags.CHANGE_NICKNAME |
+  flags.CONNECT |
+  flags.SPEAK |
+  flags.EMBED_LINKS |
+  flags.ATTACH_FILES |
+  flags.READ_MESSAGE_HISTORY;
+
+const url = `https://discordapp.com/api/oauth2/authorize?client_id=${clientId}&scope=bot&permissions=${perms}`;
+console.log(`Go to ${url} to invite the bot into a guild.`);
diff --git a/tools/adminme.ts b/tools/adminme.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60a62112881a24e6d170dd0503a403f4d37249b0
--- /dev/null
+++ b/tools/adminme.ts
@@ -0,0 +1,114 @@
+/* tslint:disable:no-console */
+/**
+ * Allows you to become an admin for a room the bot is in control of.
+ */
+
+import { Cli, Bridge, AppServiceRegistration, ClientFactory } from "matrix-appservice-bridge";
+import * as log from "npmlog";
+import * as yaml from "js-yaml";
+import * as fs from "fs";
+import * as args from "command-line-args";
+import * as usage from "command-line-usage";
+import { DiscordBridgeConfig } from "../src/config";
+
+const optionDefinitions = [
+  {
+    name: "help",
+    alias: "h",
+    type: Boolean,
+    description: "Display this usage guide."},
+  {
+    name: "config",
+    alias: "c",
+    type: String,
+    defaultValue: "config.yaml",
+    description: "The AS config file.",
+    typeLabel: "<config.yaml>" },
+  {
+    name: "roomid",
+    alias: "r",
+    type: String,
+    description: "The roomid to modify"},
+  {
+    name: "userid",
+    alias: "u",
+    type: String,
+    description: "The userid to give powers"},
+  {
+    name: "power",
+    alias: "p",
+    type: Number,
+    defaultValue: 100,
+    description: "The power to set",
+    typeLabel: "<0-100>" },
+];
+
+const options = args(optionDefinitions);
+
+if (options.help) {
+  /* tslint:disable:no-console */
+  console.log(usage([
+    {
+      header: "Admin Me",
+      content: "A tool to give a user a power level in a bot user controlled room."},
+    {
+      header: "Options",
+      optionList: optionDefinitions,
+    },
+  ]));
+  process.exit(0);
+}
+
+if (!options.roomid) {
+  console.error("Missing roomid parameter. Check -h");
+  process.exit(1);
+}
+
+if (!options.userid) {
+  console.error("Missing userid parameter. Check -h");
+  process.exit(1);
+}
+
+const yamlConfig = yaml.safeLoad(fs.readFileSync("discord-registration.yaml", "utf8"));
+const registration = AppServiceRegistration.fromObject(yamlConfig);
+const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig;
+
+if (registration === null) {
+ throw new Error("Failed to parse registration file");
+}
+
+const clientFactory = new ClientFactory({
+ appServiceUserId: "@" + registration.sender_localpart + ":" + config.bridge.domain,
+ token: registration.as_token,
+ url: config.bridge.homeserverUrl,
+});
+
+const client = clientFactory.getClientAs();
+client.startClient();
+client.on("sync", (state, prevState, data) => {
+  switch (state) {
+    case "ERROR":
+      console.error("Sync failed.", data);
+      break;
+    case "SYNCING":
+      console.log("Syncing.");
+      break;
+    case "PREPARED":
+      const room = client.getRoom(options.roomid);
+      if (room === null) {
+        console.error("Room not found.");
+        process.exit(1);
+      }
+      const levels = room.getLiveTimeline().getState("f").getStateEvents("m.room.power_levels")[0];
+      client.setPowerLevel(options.roomid, options.userid, options.power, levels).then(() => {
+        console.log("Power levels set");
+        process.exit(0);
+      }).catch((err) => {
+        console.error("Could not apply power levels: ", err);
+        process.exit(1);
+      })
+      break;
+    default:
+      break;
+  }
+});
diff --git a/tsconfig.json b/tsconfig.json
index c5651a0591da08bfac5cd8395e06276590a4c28c..a888421480d0a4e9d844699924faa847afce9fee 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,6 +11,7 @@
     "compileOnSave": true,
     "include": [
         "src/**/*",
-        "test/**/*"
+        "test/**/*",
+        "tools/**/*"
     ]
 }