diff --git a/config/config.sample.yaml b/config/config.sample.yaml
index fe0400060516ecb60e0b108f88a001d554bfe89a..59700ac4b307482296abd8b0981c2df6bf66e2c5 100644
--- a/config/config.sample.yaml
+++ b/config/config.sample.yaml
@@ -6,6 +6,7 @@ bridge:
   disableTypingNotifications: false
   disableDiscordMentions: false
   disableDeletionForwarding: false
+  enableSelfServiceBridging: false
 auth:
   clientID: "12345" # Get from discord
   secret: "blah"
diff --git a/config/config.schema.yaml b/config/config.schema.yaml
index bdb2bb5cc5c1bf24ccf82adeb82037c8800dc4e2..bb3b893a72df29e8dd9515a4c1699d1b2872a237 100644
--- a/config/config.schema.yaml
+++ b/config/config.schema.yaml
@@ -20,6 +20,8 @@ properties:
             type: "boolean"
           disableDeletionForwarding:
             type: "boolean"
+          enableSelfServiceBridging:
+            type: "boolean"
     auth:
         type: "object"
         required: ["botToken"]
diff --git a/src/bot.ts b/src/bot.ts
index ed6868460851cf3fc92f2902b19d41274a2533c0..5550402d01ee3315096799b91832c57ea6406bf0 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -12,6 +12,7 @@ import * as log from "npmlog";
 import * as Bluebird from "bluebird";
 import * as mime from "mime";
 import * as path from "path";
+import { Provisioner } from "./provisioner";
 
 // Due to messages often arriving before we get a response from the send call,
 // messages get delayed from discord.
@@ -34,7 +35,7 @@ export class DiscordBot {
   private sentMessages: string[];
   private msgProcessor: MessageProcessor;
   private presenceHandler: PresenceHandler;
-  constructor(config: DiscordBridgeConfig, store: DiscordStore) {
+  constructor(config: DiscordBridgeConfig, store: DiscordStore, private provisioner: Provisioner) {
     this.config = config;
     this.store = store;
     this.sentMessages = [];
@@ -526,6 +527,7 @@ export class DiscordBot {
 
   private async OnMessage(msg: Discord.Message) {
     const indexOfMsg = this.sentMessages.indexOf(msg.id);
+    const chan = <Discord.TextChannel> msg.channel;
     if (indexOfMsg !== -1) {
       log.verbose("DiscordBot", "Got repeated message, ignoring.");
       delete this.sentMessages[indexOfMsg];
@@ -537,7 +539,7 @@ export class DiscordBot {
     }
     // Issue #57: Detect webhooks
     if (msg.webhookID != null) {
-      const webhook = (await (<Discord.TextChannel> msg.channel).fetchWebhooks())
+      const webhook = (await chan.fetchWebhooks())
                       .filterArray((h) => h.name === "_matrix").pop();
       if (webhook != null && msg.webhookID === webhook.id) {
         // Filter out our own webhook messages.
@@ -545,6 +547,30 @@ export class DiscordBot {
       }
     }
 
+    // Check if there's an ongoing bridge request
+    if ((msg.content === "!approve" || msg.content === "!deny") && this.provisioner.HasPendingRequest(chan)) {
+      try {
+        const isApproved = msg.content === "!approve";
+        const successfullyBridged = await this.provisioner.MarkApproved(chan, msg.member, isApproved);
+        if (successfullyBridged && isApproved) {
+          msg.channel.sendMessage("Thanks for your response! The matrix bridge has been approved");
+        } else if (successfullyBridged && !isApproved) {
+          msg.channel.sendMessage("Thanks for your response! The matrix bridge has been declined");
+        } else {
+          msg.channel.sendMessage("Thanks for your response, however the time for responses has expired - sorry!");
+        }
+      } catch (err) {
+        if (err.message === "You do not have permission to manage webhooks in this channel") {
+          msg.channel.sendMessage(err.message);
+        } else {
+          log.error("DiscordBot", "Error processing room approval");
+          log.error("DiscordBot", err);
+        }
+      }
+
+      return; // stop processing - we're approving/declining the bridge request
+    }
+
     // Update presence because sometimes discord misses people.
     this.UpdateUser(msg.author).then(() => {
       return this.GetRoomIdsFromChannel(msg.channel).catch((err) => {
diff --git a/src/config.ts b/src/config.ts
index cf144ca32abef2f57339e72c4430e56bde908079..3730acf6ba642c58cd712d797e6eaf1f76bfad22 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -16,6 +16,7 @@ class DiscordBridgeConfigBridge {
   public disableTypingNotifications: boolean;
   public disableDiscordMentions: boolean;
   public disableDeletionForwarding: boolean;
+  public enableSelfServiceBridging: boolean;
 }
 
 class DiscordBridgeConfigDatabase {
diff --git a/src/discordas.ts b/src/discordas.ts
index 7ef64b6a009b91ddd030f7c992a5cd4a496113e9..2f49ea54f89734a25ce36816c23be0b1344df426 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -6,6 +6,7 @@ import { DiscordBridgeConfig } from "./config";
 import { DiscordBot } from "./bot";
 import { MatrixRoomHandler } from "./matrixroomhandler";
 import { DiscordStore } from "./store";
+import { Provisioner } from "./provisioner";
 
 const cli = new Cli({
   bridgeConfig: {
@@ -48,9 +49,10 @@ function run (port: number, config: DiscordBridgeConfig) {
     token: registration.as_token,
     url: config.bridge.homeserverUrl,
   });
+  const provisioner = new Provisioner();
   const discordstore = new DiscordStore(config.database ? config.database.filename : "discord.db");
-  const discordbot = new DiscordBot(config, discordstore);
-  const roomhandler = new MatrixRoomHandler(discordbot, config, botUserId);
+  const discordbot = new DiscordBot(config, discordstore, provisioner);
+  const roomhandler = new MatrixRoomHandler(discordbot, config, botUserId, provisioner);
 
   const bridge = new Bridge({
     clientFactory,
@@ -68,6 +70,7 @@ function run (port: number, config: DiscordBridgeConfig) {
     homeserverUrl: config.bridge.homeserverUrl,
     registration,
   });
+  provisioner.SetBridge(bridge);
   roomhandler.setBridge(bridge);
   discordbot.setBridge(bridge);
   log.info("discordas", "Initing bridge.");
diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts
index d45a964e0c79f517eb6902f1e7baf0b34e3c0c86..cee2ea5aa33448a414c56c025d34d90a90942529 100644
--- a/src/matrixroomhandler.ts
+++ b/src/matrixroomhandler.ts
@@ -2,6 +2,7 @@ import { DiscordBot } from "./bot";
 import {
   Bridge,
   RemoteRoom,
+  MatrixRoom,
   thirdPartyLookup,
   thirdPartyProtocolResult,
   thirdPartyUserResult,
@@ -12,19 +13,35 @@ import { DiscordBridgeConfig } from "./config";
 import * as Discord from "discord.js";
 import * as log from "npmlog";
 import * as Bluebird from "bluebird";
+import { Util } from "./util";
+import { Provisioner } from "./provisioner";
 
 const ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org/mlxoESwIsTbJrfXyAAogrNxA";
 const JOIN_DELAY = 6000;
 const HTTP_UNSUPPORTED = 501;
 const ROOM_NAME_PARTS = 2;
 const AGE_LIMIT = 900000; // 15 * 60 * 1000
+const PROVISIONING_DEFAULT_POWER_LEVEL = 50;
+const PROVISIONING_DEFAULT_USER_POWER_LEVEL = 0;
+
+// Note: The schedule must not have duplicate values to avoid problems in positioning.
+/* tslint:disable:no-magic-numbers */ // Disabled because it complains about the values in the array
+const JOIN_ROOM_SCHEDULE = [
+    0,              // Right away
+    1000,           // 1 second
+    30000,          // 30 seconds
+    300000,         // 5 minutes
+    900000,         // 15 minutes
+];
+/* tslint:enable:no-magic-numbers */
 
 export class MatrixRoomHandler {
+
   private config: DiscordBridgeConfig;
   private bridge: Bridge;
   private discord: DiscordBot;
   private botUserId: string;
-  constructor (discord: DiscordBot, config: DiscordBridgeConfig, botUserId: string) {
+  constructor (discord: DiscordBot, config: DiscordBridgeConfig, botUserId: string, private provisioner: Provisioner) {
     this.discord = discord;
     this.config = config;
     this.botUserId = botUserId;
@@ -74,19 +91,179 @@ export class MatrixRoomHandler {
       this.HandleInvite(event);
     } else if (event.type === "m.room.redaction" && context.rooms.remote) {
       this.discord.ProcessMatrixRedact(event);
-    } else if (event.type === "m.room.message" && context.rooms.remote) {
+    } else if (event.type === "m.room.message") {
       log.verbose("MatrixRoomHandler", "Got m.room.message event");
-      const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS);
-      return this.discord.ProcessMatrixMsgEvent(event, srvChanPair[0], srvChanPair[1]).catch((err) => {
-        log.warn("MatrixRoomHandler", "There was an error sending a matrix event", err);
-      });
+      if (event.content.body && event.content.body.startsWith("!discord")) {
+        return this.ProcessCommand(event, context);
+      } else if (context.rooms.remote) {
+        const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS);
+        return this.discord.ProcessMatrixMsgEvent(event, srvChanPair[0], srvChanPair[1]).catch((err) => {
+            log.warn("MatrixRoomHandler", "There was an error sending a matrix event", err);
+        });
+      }
     } else {
       log.verbose("MatrixRoomHandler", "Got non m.room.message event");
     }
   }
 
   public HandleInvite(event: any) {
-    // Do nothing yet.
+    log.info("MatrixRoomHandler", "Received invite for " + event.state_key + " in room " + event.room_id);
+    if (event.state_key === this.botUserId) {
+      log.info("MatrixRoomHandler", "Accepting invite for bridge bot");
+      return this.joinRoom(this.bridge.getIntent(), event.room_id);
+    }
+  }
+
+  public async ProcessCommand(event: any, context: any) {
+      if (!this.config.bridge.enableSelfServiceBridging) {
+          // We can do this here because the only commands we support are self-service bridging
+          return this.bridge.getIntent().sendMessage(event.room_id, {
+              msgtype: "m.notice",
+              body: "The owner of this bridge does not permit self-service bridging.",
+          });
+      }
+
+      // Check to make sure the user has permission to do anything in the room. We can do this here
+      // because the only commands we support are self-service commands (which therefore require some
+      // level of permissions)
+      const plEvent = await this.bridge.getIntent().getClient().getStateEvent(event.room_id, "m.room.power_levels", "");
+      let userLevel = PROVISIONING_DEFAULT_USER_POWER_LEVEL;
+      let requiredLevel = PROVISIONING_DEFAULT_POWER_LEVEL;
+      if (plEvent && plEvent.state_default) {
+          requiredLevel = plEvent.state_default;
+      }
+      if (plEvent && plEvent.users_default) {
+          userLevel = plEvent.users_default;
+      }
+      if (plEvent && plEvent.users && plEvent.users[event.sender]) {
+          userLevel = plEvent.users[event.sender];
+      }
+
+      if (userLevel < requiredLevel) {
+          return this.bridge.getIntent().sendMessage(event.room_id, {
+              msgtype: "m.notice",
+              body: "You do not have the required power level in this room to create a bridge to a Discord channel.",
+          });
+      }
+
+      const prefix = "!discord ";
+      let command = "help";
+      let args = [];
+      if (event.content.body.length >= prefix.length) {
+          const allArgs = event.content.body.substring(prefix.length).split(" ");
+          if (allArgs.length && allArgs[0] !== "") {
+              command = allArgs[0];
+              allArgs.splice(0, 1);
+              args = allArgs;
+          }
+      }
+
+      if (command === "help" && args[0] === "bridge") {
+          const link = Util.GetBotLink(this.config);
+          this.bridge.getIntent().sendMessage(event.room_id, {
+              msgtype: "m.notice",
+              body: "How to bridge a Discord guild:\n" +
+              "1. Invite the bot to your Discord guild using this link: " + link + "\n" +
+              "2. Invite me to the matrix room you'd like to bridge\n" +
+              "3. Open the Discord channel you'd like to bridge in a web browser\n" +
+              "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " +
+              "(without the backticks)\n" +
+              "   Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" +
+              "   The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" +
+              "5. Enjoy your new bridge!",
+          });
+      } else if (command === "bridge") {
+          if (context.rooms.remote) {
+              return this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "This room is already bridged to a Discord guild.",
+              });
+          }
+
+          const minArgs = 2;
+          if (args.length < minArgs) {
+              return this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "Invalid syntax. For more information try !discord help bridge",
+              });
+          }
+
+          const guildId = args[0];
+          const channelId = args[1];
+          try {
+              const discordResult = await this.discord.LookupRoom(guildId, channelId);
+              const channel = <Discord.TextChannel> discordResult.channel;
+
+              log.info("MatrixRoomHandler", `Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`);
+              this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "I'm asking permission from the guild administrators to make this bridge.",
+              });
+
+              await this.provisioner.AskBridgePermission(channel, event.sender);
+              this.provisioner.BridgeMatrixRoom(channel, event.room_id);
+              return this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "I have bridged this room to your channel",
+              });
+          } catch (err) {
+              if (err.message === "Timed out waiting for a response from the Discord owners"
+                  || err.message === "The bridge has been declined by the Discord guild") {
+                  return this.bridge.getIntent().sendMessage(event.room_id, {
+                      msgtype: "m.notice",
+                      body: err.message,
+                  });
+              }
+
+              log.error("MatrixRoomHandler", `Error bridging ${event.room_id} to ${guildId}/${channelId}`);
+              log.error("MatrixRoomHandler", err);
+              return this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "There was a problem bridging that channel - has the guild owner approved the bridge?",
+              });
+          }
+      } else if (command === "unbridge") {
+          const remoteRoom = context.rooms.remote;
+
+          if (!remoteRoom) {
+              return this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "This room is not bridged.",
+              });
+          }
+
+          if (!remoteRoom.data.plumbed) {
+              return this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "This room cannot be unbridged.",
+              });
+          }
+
+          try {
+              await this.provisioner.UnbridgeRoom(remoteRoom);
+              return this.bridge.getIntent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "This room has been unbridged",
+              });
+          } catch (err) {
+              log.error("MatrixRoomHandler", "Error while unbridging room " + event.room_id);
+              log.error("MatrixRoomHandler", err);
+              return this.bridge.getItent().sendMessage(event.room_id, {
+                  msgtype: "m.notice",
+                  body: "There was an error unbridging this room. " +
+                    "Please try again later or contact the bridge operator.",
+              });
+          }
+      } else if (command === "help") {
+          // Unknown command or no command given to get help on, so we'll just give them the help
+          this.bridge.getIntent().sendMessage(event.room_id, {
+              msgtype: "m.notice",
+              body: "Available commands:\n" +
+              "!discord bridge <guild id> <channel id>   - Bridges this room to a Discord channel\n" +
+              "!discord unbridge                         - Unbridges a Discord channel from this room\n" +
+              "!discord help <command>                   - Help menu for another command. Eg: !discord help bridge\n",
+          });
+      }
   }
 
   public OnAliasQuery (alias: string, aliasLocalpart: string): Promise<any> {
@@ -167,6 +344,25 @@ export class MatrixRoomHandler {
     return Promise.reject({err: "Unsupported", code: HTTP_UNSUPPORTED});
   }
 
+  private joinRoom(intent: any, roomIdOrAlias: string): Promise<string> {
+      let currentSchedule = JOIN_ROOM_SCHEDULE[0];
+      const doJoin = () => Util.DelayedPromise(currentSchedule).then(() => intent.getClient().joinRoom(roomIdOrAlias));
+      const errorHandler = (err) => {
+          log.error("MatrixRoomHandler", `Error joining room ${roomIdOrAlias} as ${intent.getClient().getUserId()}`);
+          log.error("MatrixRoomHandler", err);
+          const idx = JOIN_ROOM_SCHEDULE.indexOf(currentSchedule);
+          if (idx === JOIN_ROOM_SCHEDULE.length - 1) {
+              log.warn("MatrixRoomHandler", `Cannot join ${roomIdOrAlias} as ${intent.getClient().getUserId()}`);
+              return Promise.reject(err);
+          } else {
+              currentSchedule = JOIN_ROOM_SCHEDULE[idx + 1];
+              return doJoin().catch(errorHandler);
+          }
+      };
+
+      return doJoin().catch(errorHandler);
+  }
+
   private createMatrixRoom (channel: Discord.TextChannel, alias: string) {
     const remote = new RemoteRoom(`discord_${channel.guild.id}_${channel.id}`);
     remote.set("discord_type", "text");
diff --git a/src/provisioner.ts b/src/provisioner.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c8a2e0d5cb559b0f79173f493058ca3979a381af
--- /dev/null
+++ b/src/provisioner.ts
@@ -0,0 +1,87 @@
+import {
+    Bridge,
+    RemoteRoom,
+    MatrixRoom,
+} from "matrix-appservice-bridge";
+import * as Discord from "discord.js";
+import { Permissions } from "discord.js";
+
+const PERMISSION_REQUEST_TIMEOUT = 300000; // 5 minutes
+
+export class Provisioner {
+
+    private bridge: Bridge;
+    private pendingRequests: { [channelId: string]: (approved: boolean) => void } = {}; // [channelId]: resolver fn
+
+    public SetBridge(bridge: Bridge): void {
+        this.bridge = bridge;
+    }
+
+    public BridgeMatrixRoom(channel: Discord.TextChannel, roomId: string) {
+        const remote = new RemoteRoom(`discord_${channel.guild.id}_${channel.id}_bridged`);
+        remote.set("discord_type", "text");
+        remote.set("discord_guild", channel.guild.id);
+        remote.set("discord_channel", channel.id);
+        remote.set("plumbed", true);
+
+        const local = new MatrixRoom(roomId);
+        this.bridge.getRoomStore().linkRooms(local, remote);
+        this.bridge.getRoomStore().setMatrixRoom(local); // Needs to be done after linking
+    }
+
+    public UnbridgeRoom(remoteRoom: RemoteRoom) {
+        return this.bridge.getRoomStore().removeEntriesByRemoteRoomId(remoteRoom.getId());
+    }
+
+    public AskBridgePermission(channel: Discord.TextChannel, requestor: string): Promise<any> {
+        return new Promise((resolve, reject) => {
+            const channelId = channel.guild.id + "/" + channel.id;
+
+            let responded = false;
+            const approveFn = (approved: boolean, expired = false) => {
+                if (responded) {
+                    return;
+                }
+
+                responded = true;
+                delete this.pendingRequests[channelId];
+                if (approved) {
+                    resolve();
+                } else {
+                    if (expired) {
+                        reject(new Error("Timed out waiting for a response from the Discord owners"));
+                    } else {
+                        reject(new Error("The bridge has been declined by the Discord guild"));
+                    }
+                }
+            };
+
+            this.pendingRequests[channelId] = approveFn;
+            setTimeout(() => approveFn(false, true), PERMISSION_REQUEST_TIMEOUT);
+
+            channel.sendMessage(requestor + " on matrix would like to bridge this channel. Someone with permission" +
+                " to manage webhooks please reply with !approve or !deny in the next 5 minutes");
+        });
+    }
+
+    public HasPendingRequest(channel: Discord.TextChannel): boolean {
+        const channelId = channel.guild.id + "/" + channel.id;
+        return !!this.pendingRequests[channelId];
+    }
+
+    public MarkApproved(channel: Discord.TextChannel, member: Discord.GuildMember, allow: boolean): Promise<boolean> {
+        const channelId = channel.guild.id + "/" + channel.id;
+        if (!this.pendingRequests[channelId]) {
+            return Promise.resolve(false); // no change, so false
+        }
+
+        const perms = channel.permissionsFor(member);
+        if (!perms.hasPermission(Permissions.FLAGS.MANAGE_WEBHOOKS)) {
+            // Missing permissions, so just reject it
+            return Promise.reject(new Error("You do not have permission to manage webhooks in this channel"));
+        }
+
+        this.pendingRequests[channelId](allow);
+        return Promise.resolve(true); // replied, so true
+    }
+}
diff --git a/src/util.ts b/src/util.ts
index 0b15c98f637081ee9cad0662a506223517ae806a..fb2a69a7b9c2acf5b06bc95ae31ba3ac42cbfb43 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -4,6 +4,7 @@ import { Intent } from "matrix-appservice-bridge";
 import { Buffer } from "buffer";
 import * as log from "npmlog";
 import * as mime from "mime";
+import { Permissions } from "discord.js";
 
 const HTTP_OK = 200;
 
@@ -100,6 +101,35 @@ export class Util {
       throw reason;
     });
   }
+
+  /**
+   * Gets a promise that will resolve after the given number of milliseconds
+   * @param {number} duration The number of milliseconds to wait
+   * @returns {Promise<any>} The promise
+   */
+  public static DelayedPromise(duration: number): Promise<any> {
+    return new Promise<any>((resolve, reject) => {
+      setTimeout(resolve, duration);
+    });
+  }
+
+  public static GetBotLink(config: any): string {
+    /* tslint:disable:no-bitwise */
+    const perms = Permissions.FLAGS.READ_MESSAGES |
+      Permissions.FLAGS.SEND_MESSAGES |
+      Permissions.FLAGS.CHANGE_NICKNAME |
+      Permissions.FLAGS.CONNECT |
+      Permissions.FLAGS.SPEAK |
+      Permissions.FLAGS.EMBED_LINKS |
+      Permissions.FLAGS.ATTACH_FILES |
+      Permissions.FLAGS.READ_MESSAGE_HISTORY |
+      Permissions.FLAGS.MANAGE_WEBHOOKS;
+    /* tslint:enable:no-bitwise */
+
+    const clientId = config.auth.clientID;
+
+    return `https://discordapp.com/api/oauth2/authorize?client_id=${clientId}&scope=bot&permissions=${perms}`;
+  }
 }
 
 interface IUploadResult {
diff --git a/tools/addbot.ts b/tools/addbot.ts
index 425b83e81025b045c287c54cf23e2a4bf04229d6..6339d848af149e19a8954918e64eb29b66c16664 100644
--- a/tools/addbot.ts
+++ b/tools/addbot.ts
@@ -4,24 +4,12 @@
  */
 import * as yaml from "js-yaml";
 import * as fs from "fs";
-import { Permissions } from "discord.js";
+import { Util } from "../src/util";
 
-const flags = Permissions.FLAGS;
 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 |
-  flags.MANAGE_WEBHOOKS;
-
-const url = `https://discordapp.com/api/oauth2/authorize?client_id=${clientId}&scope=bot&permissions=${perms}`;
+const url = Util.GetBotLink(yamlConfig);
 console.log(`Go to ${url} to invite the bot into a guild.`);