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}`); });