diff --git a/client/src/App.svelte b/client/src/App.svelte
index e0139d3b5c2eecb9795ec790d010bf9a8e5d5f9e..b37c91b84c525f09df1df53abbd22013545e4042 100644
--- a/client/src/App.svelte
+++ b/client/src/App.svelte
@@ -1,8 +1,9 @@
 <script>
 	import AttachRoomPanel from './components/AttachRoomPanel.svelte';
 	import ChatBroadcastPanel from './components/ChatBroadcastPanel.svelte';
-	import RoomTiles from './components/RoomTiles.svelte'
-	import BulkCreatePanel from './components/BulkCreatePanel.svelte'
+	import RoomTiles from './components/RoomTiles.svelte';
+  import BulkCreatePanel from './components/BulkCreatePanel.svelte';
+  import UploadPresentationPanel from './components/UploadPresentationPanel.svelte';
 
 	import { rooms } from './stores.js';
 </script>
@@ -12,8 +13,9 @@
         <div class="columns">
           <div class="column is-one-quarter" id="sidebar">
             <AttachRoomPanel/>
-			<ChatBroadcastPanel/>
-			<BulkCreatePanel/>
+			      <ChatBroadcastPanel/>
+			      <BulkCreatePanel/>
+            <UploadPresentationPanel/>
           </div>
           <div class="column" id="main-content">
             <RoomTiles rooms={$rooms}/>
diff --git a/client/src/components/BulkCreatePanel.svelte b/client/src/components/BulkCreatePanel.svelte
index efe0c567f736df293f2afe9eaaef19fe4508eb66..46f2fa61f957a105412a958a53c6fa22af285979 100644
--- a/client/src/components/BulkCreatePanel.svelte
+++ b/client/src/components/BulkCreatePanel.svelte
@@ -1,6 +1,10 @@
 <script>
   let prefix = "";
   let amount;
+
+  async function handleBulkCreate() {
+    alert('Not yet implemented!')
+  }
 </script>
 <nav class="panel">
   <p class="panel-heading">
@@ -18,9 +22,9 @@
         <input class="input" type="text" placeholder="Amount" bind:value={amount} />
       </p>
       <p class="control">
-        <a class="button is-success is-outlined">
+        <button class="button is-success is-outlined" on:click={handleBulkCreate}>
           Create
-        </a>
+        </button>
       </p>
     </div>
     <br />
diff --git a/client/src/components/RoomTile.svelte b/client/src/components/RoomTile.svelte
index c88f1a9035c13e841f756a7724b42e32cb2138a3..f0f6724713123c139c0f245c9dc93381d0afb4c9 100644
--- a/client/src/components/RoomTile.svelte
+++ b/client/src/components/RoomTile.svelte
@@ -6,7 +6,7 @@
     export let url;
     export let name = '???';
     export let userCount = -1;
-    export let users = [];
+    export const users = [];
 
     function handleDetachClick() {
       roomsToDetachFrom.update((rooms) => [...rooms, uid]);
@@ -29,7 +29,7 @@
                   </div>
                   <footer class="card-footer">
                     <span href="#" class="card-footer-item">{userCount} 👤</span>
-                    <a href="#" on:click={handleDetachClick} class="card-footer-item">Detach</a>
+                    <button on:click={handleDetachClick} class="card-footer-item">Detach</button>
                     <a href={url} class="card-footer-item">Join</a>
                   </footer>
                 </div>
\ No newline at end of file
diff --git a/client/src/components/UploadPresentationPanel.svelte b/client/src/components/UploadPresentationPanel.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..38a550c0c9ea65a165855e01ba4cdffa891c3227
--- /dev/null
+++ b/client/src/components/UploadPresentationPanel.svelte
@@ -0,0 +1,26 @@
+<script>
+
+  import { API_BASE_URL } from '../stores.js';
+</script>
+
+<nav class="panel">
+  <p class="panel-heading">
+    Upload presentation
+  </p>
+  <div class="panel-block">
+    <form action="{API_BASE_URL}/uploadPresentation" method="post" enctype="multipart/form-data">
+      <p class="control">
+        <input type="file" name="presentation" multiple="" max="10" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.rtf,.odt,.ods,.odp,.odg,.odc,.odi,.jpg,.jpeg,.png"/>  
+      </p>
+      <p class="control">
+        <input type="submit"/>
+      </p>
+    </form>
+    <br />
+  </div>
+  <div class="panel-block">
+    <button type="submit" class="button is-link is-outlined is-fullwidth" disabled={false} on:click={() => {}}>
+      Send to attached rooms
+    </button>
+  </div>
+</nav>
diff --git a/server/bbb.js b/server/bbb.js
new file mode 100644
index 0000000000000000000000000000000000000000..1df04589d33d3ae29777e7de80462da4f8efeb62
--- /dev/null
+++ b/server/bbb.js
@@ -0,0 +1,215 @@
+const puppeteer = require("puppeteer");
+const sleep = require("sleep-promise");
+
+module.exports.createFreshPage = async function createFreshPage() {
+  const browser = await puppeteer.launch({
+    headless: HEADLESS,
+    defaultViewport: {
+      width: 1200,
+      height: 600,
+      isLandscape: true,
+    },
+  });
+  const context = await browser.createIncognitoBrowserContext();
+  return context.newPage();
+};
+
+module.exports.joinRoom = async function joinRoom(
+  page,
+  roomUrl,
+  displayName = "Puppet"
+) {
+  await page.goto(roomUrl);
+  console.log("joinRoom > Gone to start page for", roomUrl);
+
+  // Set displayname for program
+  await page.type("input.join-form", displayName);
+
+  // Join and load room
+  await page.click("button#room-join");
+  //await sleep(3000);
+
+  const dismissButton = await page.waitForSelector(
+    'button[aria-describedBy="modalDismissDescription"]'
+  );
+  console.log("joinRoom > Joined room");
+  // Click audio choice panel away
+  await dismissButton.click();
+
+  console.log("joinRoom > clicked audio choice away.");
+  return page;
+};
+
+module.exports.sendChatMessage = async function sendChatMessage(
+  page,
+  messageText
+) {
+  const chatInputHandle = await page.$("textarea#message-input");
+  const chatSendButtonHandle = await page.$(
+    'form > div > button[type="submit"]'
+  );
+
+  // Send a text message
+  await chatInputHandle.type(messageText);
+  await chatSendButtonHandle.click();
+  console.log("sendChatMessage > sent Message");
+};
+
+module.exports.signIn = async function signIn(
+  bbbUrl,
+  username,
+  password,
+  page = null
+) {
+  // Create page if not done already
+  if (page === null) {
+    page = await createFreshPage();
+  }
+
+  await page.goto(bbbUrl);
+
+  await page.waitForSelector("a.sign-in-button");
+  const signInButton = await page.$("a.sign-in-button");
+  if (signInButton === null) {
+    console.log(
+      "SignIn > Can't find sign-in button: User is probably already authenticated."
+    );
+    return page;
+  }
+
+  await page.click("a.sign-in-button");
+  await page.waitForNavigation();
+  console.log("SignIn > NAVIGATED TO LOGIN PAGE");
+
+  await page.type("input#session_username", username);
+  await page.type("input#session_password", password);
+  await page.click("input.signin-button");
+  console.log("SignIn > entered credentials");
+
+  await page.waitForNavigation();
+  console.log("SignIn > Probably logged in.");
+
+  return page;
+};
+
+module.exports.screenShotPresentationArea = async function screenShotPresentationArea(
+  authenticatedPage,
+  path
+) {
+  const presentationAreaHandle = await authenticatedPage.$("div#container");
+  presentationAreaHandle.screenshot({ path, type: "jpeg", quality: 33 });
+};
+
+module.exports.getUserCount = async function getUserCount(authenticatedPage) {
+  const selector = 'div[role="complementary"] > div > div > h2';
+  const h2Handle = (await authenticatedPage.$$(selector))[2];
+  return h2Handle.evaluate(
+    (node) => node.innerText.split("(")[1].split(")")[0]
+  );
+};
+
+module.exports.getUserList = async function getUserList(authenticatedPage) {
+  const users = [];
+
+  const selector = 'div[class*="userItemContents"]';
+  const userHandles = await authenticatedPage.$$(selector);
+  for (const userHandle of userHandles) {
+    const avatarHandle = await userHandle.$('div[class*="avatar"]');
+    const classes = (await avatarHandle.evaluate((node) => node.className))
+      .split(" ")
+      .map((wholeClass) => wholeClass.split("--")[0])
+      .filter((name) => name !== "avatar");
+    const userNameHandle = await userHandle.$(
+      'span[class*="userNameMain"] > span'
+    );
+    const userName = await userNameHandle.evaluate((node) => node.innerText);
+
+    users.push({
+      classes,
+      name: userName,
+    });
+  }
+  return users;
+};
+
+module.exports.getRoomName = async function getRoomName(authenticatedPage) {
+  const roomNameHandle = await authenticatedPage.$(
+    'h1[class*="presentationTitle"]'
+  );
+  if (roomNameHandle === null) {
+    process.send({ eventName: "sessionClosed", data: null });
+    await sleep(100);
+    process.exit(0);
+  }
+  return await roomNameHandle.evaluate((node) => node.innerText);
+};
+
+module.exports.createRoom = async function createRoom(
+  authenticatedPage,
+  roomSettings
+) {
+  const pageUrl = await authenticatedPage.url();
+  const urlparts = pageUrl.split("/");
+  const url = urlparts[0] + "//" + urlparts[2];
+  const authenticityTokenHandle = await authenticatedPage.$(
+    'input[name="authenticity_token"]'
+  );
+  const authenticityToken = await authenticityTokenHandle.evaluate(
+    (node) => node.value
+  );
+
+  await authenticatedPage.evaluate(
+    async (url, authenticityToken, roomSettings) => {
+      function urlencodeFormData(fd) {
+        var s = "";
+        function encode(s) {
+          return encodeURIComponent(s).replace(/%20/g, "+");
+        }
+        for (var pair of fd.entries()) {
+          if (typeof pair[1] == "string") {
+            s += (s ? "&" : "") + encode(pair[0]) + "=" + encode(pair[1]);
+          }
+        }
+        return s;
+      }
+
+      // Prepare form data
+      const form = new FormData();
+      form.set("utf8", "✓");
+      form.set("authenticity_token", authenticityToken);
+      for (const key of Object.keys(roomSettings)) {
+        // Maybe fix this, because the POST format is weird
+        if (typeof roomSettings[key] === "boolean") {
+          form.append("room[" + key + "]", 0);
+          if (roomSettings[key]) {
+            form.append("room[" + key + "]", 1);
+          }
+        } else {
+          form.set("room[" + key + "]", roomSettings[key]);
+        }
+      }
+      form.set("room[auto_join]", "0");
+      form.set("commit", "Create+Room");
+
+      try {
+        // Send request to create room
+        const response = await fetch(url, {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/x-www-form-urlencoded",
+          },
+          redirect: "follow", // manual, *follow, error
+          body: urlencodeFormData(form),
+        });
+        return response;
+      } catch (error) {
+        return error;
+      }
+    },
+    url + "/b",
+    authenticityToken,
+    roomSettings
+  );
+
+  await authenticatedPage.goto(url);
+};
diff --git a/server/index..js b/server/index..js
index 474537b7fd857196c19457d8cdbecbb4a10b1df6..d8e541dcde312297a2f671892a49ec16797ca7eb 100644
--- a/server/index..js
+++ b/server/index..js
@@ -7,8 +7,8 @@ const chromiumPath =
 const botDisplayName = "Puppet";
 const bbbUrl = "https://bbb.fachschaften.org";
 const roomUrl = "https://bbb.fachschaften.org/b/jon-kmr-9gk";
-const username = "tudo-fsinfo-fsr-bot";
-const password = "S4VKuhkASUWsTgL8";
+const username = "username";
+const password = "password";
 
 (async () => {
   const browser = await puppeteer.launch({
diff --git a/server/package-lock.json b/server/package-lock.json
index 6a822473d1784a6b942e0b0c756890b7e683fa69..69356cf1ed32db41fa264188fec1037fcd4815f9 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -33,6 +33,11 @@
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
       "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
     },
+    "append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
+    },
     "array-flatten": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -113,6 +118,38 @@
       "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
       "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
     },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+    },
+    "busboy": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
+      "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
+      "requires": {
+        "dicer": "0.2.5",
+        "readable-stream": "1.1.x"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "bytes": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -128,6 +165,51 @@
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
     },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        },
+        "readable-stream": {
+          "version": "2.3.7",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+          "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
     "content-disposition": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@@ -158,6 +240,11 @@
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
       "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
     },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
     "cors": {
       "version": "2.8.5",
       "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -190,6 +277,33 @@
       "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.781568.tgz",
       "integrity": "sha512-9Uqnzy6m6zEStluH9iyJ3iHyaQziFnMnLeC8vK0eN6smiJmIx7+yB64d67C2lH/LZra+5cGscJAJsNXO+MdPMg=="
     },
+    "dicer": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+      "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
+      "requires": {
+        "readable-stream": "1.1.x",
+        "streamsearch": "0.1.2"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -433,6 +547,11 @@
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
       "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
     },
+    "isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+    },
     "locate-path": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -482,6 +601,19 @@
         "brace-expansion": "^1.1.7"
       }
     },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+    },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
     "mkdirp-classic": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -492,6 +624,21 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "multer": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
+      "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
+      "requires": {
+        "append-field": "^1.0.0",
+        "busboy": "^0.2.11",
+        "concat-stream": "^1.5.2",
+        "mkdirp": "^0.5.1",
+        "object-assign": "^4.1.1",
+        "on-finished": "^2.3.0",
+        "type-is": "^1.6.4",
+        "xtend": "^4.0.0"
+      }
+    },
     "negotiator": {
       "version": "0.6.2",
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@@ -572,6 +719,17 @@
         "find-up": "^4.0.0"
       }
     },
+    "prettier": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
+      "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+    },
     "progress": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -741,6 +899,11 @@
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
       "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
     },
+    "streamsearch": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+      "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+    },
     "string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -791,6 +954,11 @@
         "mime-types": "~2.1.24"
       }
     },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
     "unbzip2-stream": {
       "version": "1.4.3",
       "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
@@ -830,6 +998,11 @@
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
       "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
     },
+    "xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
+    },
     "yauzl": {
       "version": "2.10.0",
       "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
diff --git a/server/package.json b/server/package.json
index 94e110524bdb55c559ce5791287967a080429fc1..f4c70067c95e175f22f0504135e741c1c8c0c447 100644
--- a/server/package.json
+++ b/server/package.json
@@ -5,12 +5,15 @@
   "main": "index..js",
   "dependencies": {
     "body-parser": "^1.19.0",
-    "express": "^4.17.1",
     "cors": "^2.8.5",
+    "express": "^4.17.1",
+    "multer": "^1.4.2",
     "puppeteer": "^5.2.1",
     "sleep-promise": "^8.0.1"
   },
-  "devDependencies": {},
+  "devDependencies": {
+    "prettier": "^2.1.2"
+  },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "start": "node server.js"
diff --git a/server/room_attacher.js b/server/room_attacher.js
index bd91e262fc385456c9ce6b40e9acc4699fa508d4..0a43a517cf414293dfcd7130eebf0f200f8f3d22 100644
--- a/server/room_attacher.js
+++ b/server/room_attacher.js
@@ -1,7 +1,7 @@
-const puppeteer = require("puppeteer");
 const sleep = require("sleep-promise");
 const { clearInterval } = require("timers");
 const fs = require("fs");
+const bbb = require("./bbb.js");
 
 const HEADLESS =
   process.env.HEADLESS !== undefined ? process.env.HEADLESS : true;
@@ -49,9 +49,18 @@ async function handleAttachToRoom({
   stateRoomUrl = roomUrl;
   try {
     // Attach browser to room
-    const page = await createFreshPage();
-    const authenticatedPage = await signIn(roomUrl, username, password, page);
-    const pageInRoom = await joinRoom(authenticatedPage, roomUrl, displayName);
+    const page = await bbb.createFreshPage();
+    const authenticatedPage = await bbb.signIn(
+      roomUrl,
+      username,
+      password,
+      page
+    );
+    const pageInRoom = await bbb.joinRoom(
+      authenticatedPage,
+      roomUrl,
+      displayName
+    );
     roomPage = pageInRoom;
 
     // Set up periodic screenshotting
@@ -81,7 +90,7 @@ async function handleAttachToRoom({
 
 async function handleTakeScreenshot(pathToSaveTo) {
   try {
-    await screenShotPresentationArea(roomPage, pathToSaveTo);
+    await bbb.screenShotPresentationArea(roomPage, pathToSaveTo);
   } catch (error) {
     console.warn("Unable to take screenshot of room", stateRoomUrl);
   }
@@ -90,9 +99,9 @@ async function handleTakeScreenshot(pathToSaveTo) {
 async function handleGetRoomInfo() {
   try {
     const promises = [
-      getUserCount(roomPage),
-      getUserList(roomPage),
-      getRoomName(roomPage),
+      bbb.getUserCount(roomPage),
+      bbb.getUserList(roomPage),
+      bbb.getRoomName(roomPage),
     ];
     const userCount = await promises[0];
     const userList = await promises[1];
@@ -115,7 +124,7 @@ async function handleSendMessage({ content }) {
     data: "Received content to send to chat: " + content,
   });
   try {
-    await sendChatMessage(roomPage, content);
+    await bbb.sendChatMessage(roomPage, content);
     process.send({ eventName: "sendMessageSuccess", data: null });
   } catch (error) {
     console.error(error);
@@ -134,134 +143,3 @@ async function handleDetach() {
   await sleep(100);
   process.exit(0);
 }
-// ----------------------------
-// Utility functions
-// ----------------------------
-
-async function createFreshPage() {
-  const browser = await puppeteer.launch({
-    headless: HEADLESS,
-    defaultViewport: {
-      width: 1200,
-      height: 600,
-      isLandscape: true,
-    },
-  });
-  const context = await browser.createIncognitoBrowserContext();
-  return context.newPage();
-}
-
-async function joinRoom(page, roomUrl, displayName = "Puppet") {
-  await page.goto(roomUrl);
-  console.log("joinRoom > Gone to start page for", roomUrl);
-
-  // Set displayname for program
-  await page.type("input.join-form", displayName);
-
-  // Join and load room
-  await page.click("button#room-join");
-  //await sleep(3000);
-
-  const dismissButton = await page.waitForSelector(
-    'button[aria-describedBy="modalDismissDescription"]'
-  );
-  console.log("joinRoom > Joined room");
-  // Click audio choice panel away
-  await dismissButton.click();
-
-  console.log("joinRoom > clicked audio choice away.");
-  return page;
-}
-
-async function sendChatMessage(page, messageText) {
-  const chatInputHandle = await page.$("textarea#message-input");
-  const chatSendButtonHandle = await page.$(
-    'form > div > button[type="submit"]'
-  );
-
-  // Send a text message
-  await chatInputHandle.type(messageText);
-  await chatSendButtonHandle.click();
-  console.log("sendChatMessage > sent Message");
-}
-
-async function signIn(bbbUrl, username, password, page = null) {
-  // Create page if not done already
-  if (page === null) {
-    page = await createFreshPage();
-  }
-
-  await page.goto(bbbUrl);
-
-  await page.waitForSelector("a.sign-in-button");
-  const signInButton = await page.$("a.sign-in-button");
-  if (signInButton === null) {
-    console.log(
-      "SignIn > Can't find sign-in button: User is probably already authenticated."
-    );
-    return page;
-  }
-
-  await page.click("a.sign-in-button");
-  await page.waitForNavigation();
-  console.log("SignIn > NAVIGATED TO LOGIN PAGE");
-
-  await page.type("input#session_username", username);
-  await page.type("input#session_password", password);
-  await page.click("input.signin-button");
-  console.log("SignIn > entered credentials");
-
-  await page.waitForNavigation();
-  console.log("SignIn > Probably logged in.");
-
-  return page;
-}
-
-async function screenShotPresentationArea(authenticatedPage, path) {
-  const presentationAreaHandle = await authenticatedPage.$("div#container");
-  presentationAreaHandle.screenshot({ path, type: "jpeg", quality: 33 });
-}
-
-async function getUserCount(authenticatedPage) {
-  const selector = 'div[role="complementary"] > div > div > h2';
-  const h2Handle = (await authenticatedPage.$$(selector))[2];
-  return h2Handle.evaluate(
-    (node) => node.innerText.split("(")[1].split(")")[0]
-  );
-}
-
-async function getUserList(authenticatedPage) {
-  const users = [];
-
-  const selector = 'div[class*="userItemContents"]';
-  const userHandles = await authenticatedPage.$$(selector);
-  for (const userHandle of userHandles) {
-    const avatarHandle = await userHandle.$('div[class*="avatar"]');
-    const classes = (await avatarHandle.evaluate((node) => node.className))
-      .split(" ")
-      .map((wholeClass) => wholeClass.split("--")[0])
-      .filter((name) => name !== "avatar");
-    const userNameHandle = await userHandle.$(
-      'span[class*="userNameMain"] > span'
-    );
-    const userName = await userNameHandle.evaluate((node) => node.innerText);
-
-    users.push({
-      classes,
-      name: userName,
-    });
-  }
-  return users;
-}
-
-async function getRoomName(authenticatedPage) {
-  const roomNameHandle = await authenticatedPage.$(
-    'h1[class*="presentationTitle"]'
-  );
-  if (roomNameHandle === null) {
-    process.send({ eventName: "sessionClosed", data: null });
-    await sleep(100);
-    process.exit(0);
-  }
-  return await roomNameHandle.evaluate((node) => node.innerText);
-}
diff --git a/server/server.js b/server/server.js
index f6a0078acd92277d981d227eded49a8d32477bad..f3dfb04052e8c6ae6ea29edca5bbf4da7ec81d02 100644
--- a/server/server.js
+++ b/server/server.js
@@ -4,6 +4,7 @@ const cors = require("cors");
 const path = require("path");
 const cp = require("child_process");
 const fs = require("fs");
+const multer = require("multer");
 const sleep = require("sleep-promise");
 
 const PORT = process.env.PORT || 3000;
@@ -83,6 +84,12 @@ const app = express();
 app.use(express.static("public"));
 app.use(bodyParser.json());
 app.use(cors());
+const fileUpload = multer({
+  dest: "uploads/",
+  limits: {
+    fileSize: 30 * 1000 * 1000, // ~= 30 MB
+  },
+});
 
 app.get("/api/preview/:roomUid", (req, res) => {
   res.setHeader("Cache-Control", "max-age=2");
@@ -156,6 +163,16 @@ app.post("/api/bulkcreate", async (req, res) => {
   const { prefix, amount } = req.body;
 });
 
+app.post(
+  "/api/uploadPresentation",
+  fileUpload.array("presentation", 10),
+  async (req, res, next) => {
+    console.debug(req.files);
+    res.status(200).send({}).end();
+    return;
+  }
+);
+
 app.listen(PORT, () => {
   console.log(`Listening on port ${PORT}`);
 });