commit 37cab87a6f1c2a25855ff1741e2447d24967453c Author: elijahr2411 Date: Wed Jan 25 19:55:34 2023 -0500 buncha shit uhh i forgot to make a repo until now diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d829b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/*.js \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..9b9faad --- /dev/null +++ b/dist/index.html @@ -0,0 +1,71 @@ + + + + Control Collaborative Virtual Machines! + + + + + + + +
+
+
+
+ +

+
+ + +
+
+
+
+ + + + + +
Users Online ()
+
+
+
+
+ + + + +
+
+
+ Username + +
+
+
+
+ + + + \ No newline at end of file diff --git a/dist/style.css b/dist/style.css new file mode 100644 index 0000000..5e2d4cc --- /dev/null +++ b/dist/style.css @@ -0,0 +1,48 @@ +#vmview { + display: none; +} +/*.vmtile { + text-decoration: none; + color: #FFFFFF; + font-size: 16pt; + border: 2px solid #575757; + border-radius: 15px; + height: fit-content; + width: fit-content; + display: block; + padding: 4px; +}*/ +#display, #btns { + margin-left: auto; + margin-right: auto; + text-align: center; + display: block; + margin-bottom: 10px; +} +#vmlist > div.row > div { + padding-bottom: 10px; +} +#vmlist div.col-sm-4 > div.card:hover { + cursor: pointer; + border-color: rgb(8, 121, 250); +} +.vmtile > img { + margin-bottom: 2px; +}*/ +.chat-table, .username-table { + overflow-y: auto; + border: 1px solid #575757; +} +.chat-table { + height: 30vh; +} +.username-table { + max-height: 30vh; +} +.username-table > table > thead { + position: sticky; + top: 0; +} +#turnstatus { + text-align: center; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..26545db --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "cvmwebapp", + "version": "1.0.0", + "description": "kill me", + "private": true, + "scripts": { + "build": "webpack --config webpack.config.js" + }, + "author": "Elijah R", + "license": "GPL-3.0", + "dependencies": { + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1" + } +} diff --git a/src/common.js b/src/common.js new file mode 100644 index 0000000..5fc8efc --- /dev/null +++ b/src/common.js @@ -0,0 +1,13 @@ +export const config = { + serverAddresses: [ + "wss://computernewb.com/collab-vm/vm0", + "wss://computernewb.com/collab-vm/vm1", + "wss://computernewb.com/collab-vm/vm2", + "wss://computernewb.com/collab-vm/vm3", + "wss://computernewb.com/collab-vm/vm4", + "wss://computernewb.com/collab-vm/vm5", + "wss://computernewb.com/collab-vm/vm6", + "wss://computernewb.com/collab-vm/vm7", + "wss://computernewb.com/collab-vm/vm8", + ] +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ca6bdad --- /dev/null +++ b/src/index.js @@ -0,0 +1,330 @@ +import { guacutils } from "./protocol"; +import { config } from "./common"; +import { GetKeysym } from "./keyboard"; +// None = -1 +// Has turn = 0 +// In queue = +var turn = -1; +var perms = 0; +const vms = []; +const users = []; +const buttons = { + takeTurn: window.document.getElementById("takeTurnBtn"), + changeUsername: window.document.getElementById("changeUsernameBtn") +} +var hasTurn = false; +var vm; +var connected = false; +// Elements +const turnstatus = window.document.getElementById("turnstatus"); +const vmlist = window.document.getElementById("vmlist"); +const vmview = window.document.getElementById("vmview"); +const display = window.document.getElementById("display"); +const displayCtx = display.getContext("2d"); +const chatList = window.document.getElementById("chatList"); +const userlist = window.document.getElementById("userlist"); +const usernameSpan = window.document.getElementById("username"); +const onlineusercount = window.document.getElementById("onlineusercount"); +const chatinput = window.document.getElementById("chat-input"); +// needed to scroll to bottom +const chatListDiv = document.querySelector(".chat-table"); + +class CollabVMClient { + socket; + #url; + constructor(url) { + this.#url = url; + } + connect() { + return new Promise((res, rej) => { + this.socket = new WebSocket(this.#url, "guacamole"); + this.socket.addEventListener('message', (e) => this.#onMessage(e)); + this.socket.addEventListener('open', () => res(), {once: true}); + }) + + } + disconnect() { + this.socket.send(guacutils.encode(["disconnect"])); + this.socket.close(); + } + getUrl() { + return this.#url; + } + connectToVM(node) { + return new Promise((res, rej) => { + var savedUsername = window.localStorage.getItem("username"); + if (savedUsername === null) + this.socket.send(guacutils.encode(["rename"])); + else this.socket.send(guacutils.encode(["rename", savedUsername])); + var f = (e) => { + var msgArr = guacutils.decode(e.data); + if (msgArr[0] == "connect") { + switch (msgArr[1]) { + case "0": + rej("Failed to connect to the node"); + break; + case "1": + res(); + break; + } + this.socket.removeEventListener("message", f); + } + } + this.socket.addEventListener("message", f); + this.socket.send(guacutils.encode(["connect", node])); + }); + } + async #onMessage(event) { + var msgArr = guacutils.decode(event.data); + switch (msgArr[0]) { + case "nop": + this.socket.send("3.nop;"); + break; + case "chat": + if (!connected) return; + for (var i = 1; i < msgArr.length; i += 2) { + var tr = document.createElement("tr"); + var td = document.createElement("td"); + if (msgArr[i] == "") + td.innerHTML = msgArr[i+1]; + else td.innerHTML = `${msgArr[i]}> ${msgArr[i+1]}`; + tr.appendChild(td); + chatList.appendChild(tr); + } + chatListDiv.scrollTop = chatListDiv.scrollHeight; + break; + case "size": + if (!connected || msgArr[1] !== "0") return; + display.width = msgArr[2]; + display.height = msgArr[3]; + break; + case "png": + if (!connected || msgArr[2] !== "0") return; + var img = new Image(display.width, display.height); + img.addEventListener('load', () => { + displayCtx.drawImage(img, msgArr[3], msgArr[4]); + }); + img.src = "data:image/png;base64," + msgArr[5]; + break; + case "rename": + if (msgArr[1] === "0") { + window.username = msgArr[3]; + usernameSpan.innerText = msgArr[3]; + window.localStorage.setItem("username", msgArr[3]); + } + var user = users.find(u => u.username == msgArr[2]); + if (user === undefined) break; + user.username = msgArr[3]; + user.element.children[0].innerHTML = msgArr[3]; + break; + case "adduser": + for (var i = 2; i < msgArr.length; i += 2) { + var olduser = users.find(u => u.username === msgArr[i]); + if (olduser !== undefined) { + users.splice(users.indexOf(olduser), 1); + userlist.removeChild(olduser.element); + } + var user = { + username: msgArr[i], + rank: Number(msgArr[i+1]), + turn: -1 + }; + users.push(user); + var tr = document.createElement("tr"); + var td = document.createElement("td"); + td.innerHTML = msgArr[i]; + switch (user.rank) { + case 2: + td.style.color = "#FF0000"; + break; + case 3: + td.style.color = "#00FF00"; + break; + } + tr.appendChild(td); + user.element = tr; + userlist.appendChild(tr); + } + onlineusercount.innerText = users.length; + break; + case "remuser": + for (var i = 2; i < msgArr.length; i++) { + var user = users.find(u => u.username == msgArr[i]); + users.splice(users.indexOf(user), 1); + userlist.removeChild(user.element); + } + onlineusercount.innerText = users.length; + break; + + case "turn": + // Reset all turn data + users.forEach((curr) => { + curr.turn = -1; + curr.element.classList = ""; + }); + buttons.takeTurn.innerText = "Take Turn"; + turn = -1; + turnstatus.innerText = ""; + // Get the number of users queued for a turn + var queuedUsers = Number(msgArr[2]); + if (queuedUsers === 0) return; + var currentTurnUsername = msgArr[3]; + // Get the user who has the turn and highlight them + var currentTurnUser = users.find(u => u.username === currentTurnUsername); + currentTurnUser.element.classList = "table-primary"; + currentTurnUser.turn = 0; + if (currentTurnUsername === window.username) { + turn = 0; + turnstatus.innerText = "You have the turn."; + } + // Highlight all waiting users and set their status + if (queuedUsers > 1) { + for (var i = 1; i < queuedUsers; i++) { + if (window.username === msgArr[i+3]) { + turn = i; + turnstatus.innerText = "Waiting for turn"; + }; + var user = users.find(u => u.username === msgArr[i+3]); + user.turn = i; + user.element.classList = "table-warning"; + } + } + if (turn === -1) { + buttons.takeTurn.innerText = "Take Turn"; + } else { + buttons.takeTurn.innerText = "End Turn"; + } + this.reloadUsers(); + break; + } + } + reloadUsers() { + // Sort the user list by turn status + users.sort((a, b) => { + if (a.turn === b.turn) return 0; + if (a.turn === -1) return 1; + if (b.turn === -1) return -1; + if (a.turn < b.turn) return -1; + else return 1; + }); + users.forEach((u) => { + userlist.removeChild(u.element); + userlist.appendChild(u.element); + }); + } + async list() { + return new Promise((res, rej) => { + var h = (e) => { + var msgArr = guacutils.decode(e.data); + if (msgArr[0] === "list") { + var list = []; + for (var i = 1; i < msgArr.length; i+=3) { + list.push({ + url: this.#url, + id: msgArr[i], + name: msgArr[i+1], + thumb: msgArr[i+2], + + }); + } + this.socket.removeEventListener("message", h); + res(list); + } + }; + this.socket.addEventListener("message", h); + this.socket.send("4.list;"); + }); + } + chat(msg) { + this.socket.send(guacutils.encode(["chat", msg])); + } + rename(username) { + this.socket.send(guacutils.encode(["rename", username])); + } + turn() { + if (turn === -1) { + this.socket.send(guacutils.encode(["turn", "1"])) + } else { + this.socket.send(guacutils.encode(["turn", "0"])); + } + } + mouse(x, y, mask) { + this.socket.send(guacutils.encode(["mouse", x, y, mask])); + } + key(keysym, down) { + this.socket.send(guacutils.encode(["key", keysym, down ? "1" : "0"])); + } + mouseevent(e) { + var mask = 0; + if ((e.buttons & 1) !== 0) mask |= 1; + if ((e.buttons & 4) !== 0) mask |= 2; + if ((e.buttons & 2) !== 0) mask |= 4; + this.mouse(e.offsetX, e.offsetY, mask); + } + keyevent(e, down) { + e.preventDefault(); + var keysym = GetKeysym(e.keyCode, e.keyIdentifier, e.key, e.location); + console.log(keysym); + if (keysym === undefined) return; + this.key(keysym, down); + } +} +function multicollab(url) { + return new Promise(async (res, rej) => { + var vm = new CollabVMClient(url); + await vm.connect(); + var list = await vm.list(); + vm.disconnect(); + list.forEach(curr => { + vms.push(curr); + var div = document.createElement("div"); + div.classList = "col-sm-4"; + var card = document.createElement("div"); + card.classList = "card bg-dark text-light"; + card.addEventListener("click", () => openVM(curr.url, curr.id)); + var img = document.createElement("img"); + img.src = "data:image/png;base64," + curr.thumb; + img.classList = "card-img-top"; + var bdy = document.createElement("div"); + bdy.classList = "card-body"; + var desc = document.createElement("h5"); + desc.innerHTML = curr.name; + bdy.appendChild(desc); + card.appendChild(img); + card.appendChild(bdy); + div.appendChild(card); + vmlist.children[0].appendChild(div); + }); + res(); + }); +} +async function openVM(url, node) { + vm = new CollabVMClient(url); + await vm.connect(); + connected = true; + await vm.connectToVM(node); + vmlist.style.display = "none"; + vmview.style.display = "block"; + display.addEventListener('mousemove', (e) => vm.mouseevent(e)) + display.addEventListener('mousedown', (e) => vm.mouseevent(e)); + display.addEventListener('mouseup', (e) => vm.mouseevent(e)); + display.addEventListener('contextmenu', (e) => e.preventDefault()); + display.addEventListener('click', () => { + if (turn === -1) vm.turn(); + }); + display.addEventListener('keydown', (e) => vm.keyevent(e, true)); + display.addEventListener('keyup', (e) => vm.keyevent(e, false)); +} +chatinput.addEventListener("keypress", (e) => { + if (e.key == "Enter") { + vm.chat(chatinput.value); + chatinput.value = ""; + } +}); +buttons.changeUsername.addEventListener('click', () => { + var newuser = window.prompt("Enter new username", window.username); + if (newuser == null) return; + vm.rename(newuser); +}); +buttons.takeTurn.addEventListener('click', () => vm.turn()); +config.serverAddresses.forEach(multicollab); \ No newline at end of file diff --git a/src/keyboard.js b/src/keyboard.js new file mode 100644 index 0000000..1db3267 --- /dev/null +++ b/src/keyboard.js @@ -0,0 +1,223 @@ +// Pulled a bunch of functions out of the guac source code to get a keysym +// and then a wrapper +// shitty but it works so /shrug +export function GetKeysym(keyCode, keyIdentifier, key, location) { + var keysym = keysym_from_key_identifier(key, location) + || keysym_from_keycode(keyCode, location); + + if (!keysym && key_identifier_sane(keyCode, keyIdentifier)) + keysym = keysym_from_key_identifier(keyIdentifier, location); + + return keysym; +} + + +function keysym_from_key_identifier(identifier, location) { + + if (!identifier) + return null; + + var typedCharacter; + + // If identifier is U+xxxx, decode Unicode character + var unicodePrefixLocation = identifier.indexOf("U+"); + if (unicodePrefixLocation >= 0) { + var hex = identifier.substring(unicodePrefixLocation+2); + typedCharacter = String.fromCharCode(parseInt(hex, 16)); + } + + // If single character, use that as typed character + else if (identifier.length === 1) + typedCharacter = identifier; + + // Otherwise, look up corresponding keysym + else + return get_keysym(keyidentifier_keysym[identifier], location); + + // Get codepoint + var codepoint = typedCharacter.charCodeAt(0); + return keysym_from_charcode(codepoint); + +} + +function get_keysym(keysyms, location) { + + if (!keysyms) + return null; + + return keysyms[location] || keysyms[0]; +} + +function keysym_from_charcode(codepoint) { + + // Keysyms for control characters + if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; + + // Keysyms for ASCII chars + if (codepoint >= 0x0000 && codepoint <= 0x00FF) + return codepoint; + + // Keysyms for Unicode + if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) + return 0x01000000 | codepoint; + + return null; +} + + +function isControlCharacter(codepoint) { + return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); +} + +function keysym_from_keycode(keyCode, location) { + return get_keysym(keycodeKeysyms[keyCode], location); +} + +function key_identifier_sane(keyCode, keyIdentifier) { + + // Missing identifier is not sane + if (!keyIdentifier) + return false; + + // Assume non-Unicode keyIdentifier values are sane + var unicodePrefixLocation = keyIdentifier.indexOf("U+"); + if (unicodePrefixLocation === -1) + return true; + + // If the Unicode codepoint isn't identical to the keyCode, + // then the identifier is likely correct + var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16); + if (keyCode !== codepoint) + return true; + + // The keyCodes for A-Z and 0-9 are actually identical to their + // Unicode codepoints + if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57)) + return true; + + // The keyIdentifier does NOT appear sane + return false; + +} + +var keyidentifier_keysym = { + "Again": [0xFF66], + "AllCandidates": [0xFF3D], + "Alphanumeric": [0xFF30], + "Alt": [0xFFE9, 0xFFE9, 0xFE03], + "Attn": [0xFD0E], + "AltGraph": [0xFE03], + "ArrowDown": [0xFF54], + "ArrowLeft": [0xFF51], + "ArrowRight": [0xFF53], + "ArrowUp": [0xFF52], + "Backspace": [0xFF08], + "CapsLock": [0xFFE5], + "Cancel": [0xFF69], + "Clear": [0xFF0B], + "Convert": [0xFF21], + "Copy": [0xFD15], + "Crsel": [0xFD1C], + "CrSel": [0xFD1C], + "CodeInput": [0xFF37], + "Compose": [0xFF20], + "Control": [0xFFE3, 0xFFE3, 0xFFE4], + "ContextMenu": [0xFF67], + "DeadGrave": [0xFE50], + "DeadAcute": [0xFE51], + "DeadCircumflex": [0xFE52], + "DeadTilde": [0xFE53], + "DeadMacron": [0xFE54], + "DeadBreve": [0xFE55], + "DeadAboveDot": [0xFE56], + "DeadUmlaut": [0xFE57], + "DeadAboveRing": [0xFE58], + "DeadDoubleacute": [0xFE59], + "DeadCaron": [0xFE5A], + "DeadCedilla": [0xFE5B], + "DeadOgonek": [0xFE5C], + "DeadIota": [0xFE5D], + "DeadVoicedSound": [0xFE5E], + "DeadSemivoicedSound": [0xFE5F], + "Delete": [0xFFFF], + "Down": [0xFF54], + "End": [0xFF57], + "Enter": [0xFF0D], + "EraseEof": [0xFD06], + "Escape": [0xFF1B], + "Execute": [0xFF62], + "Exsel": [0xFD1D], + "ExSel": [0xFD1D], + "F1": [0xFFBE], + "F2": [0xFFBF], + "F3": [0xFFC0], + "F4": [0xFFC1], + "F5": [0xFFC2], + "F6": [0xFFC3], + "F7": [0xFFC4], + "F8": [0xFFC5], + "F9": [0xFFC6], + "F10": [0xFFC7], + "F11": [0xFFC8], + "F12": [0xFFC9], + "F13": [0xFFCA], + "F14": [0xFFCB], + "F15": [0xFFCC], + "F16": [0xFFCD], + "F17": [0xFFCE], + "F18": [0xFFCF], + "F19": [0xFFD0], + "F20": [0xFFD1], + "F21": [0xFFD2], + "F22": [0xFFD3], + "F23": [0xFFD4], + "F24": [0xFFD5], + "Find": [0xFF68], + "GroupFirst": [0xFE0C], + "GroupLast": [0xFE0E], + "GroupNext": [0xFE08], + "GroupPrevious": [0xFE0A], + "FullWidth": null, + "HalfWidth": null, + "HangulMode": [0xFF31], + "Hankaku": [0xFF29], + "HanjaMode": [0xFF34], + "Help": [0xFF6A], + "Hiragana": [0xFF25], + "HiraganaKatakana": [0xFF27], + "Home": [0xFF50], + "Hyper": [0xFFED, 0xFFED, 0xFFEE], + "Insert": [0xFF63], + "JapaneseHiragana": [0xFF25], + "JapaneseKatakana": [0xFF26], + "JapaneseRomaji": [0xFF24], + "JunjaMode": [0xFF38], + "KanaMode": [0xFF2D], + "KanjiMode": [0xFF21], + "Katakana": [0xFF26], + "Left": [0xFF51], + "Meta": [0xFFE7, 0xFFE7, 0xFFE8], + "ModeChange": [0xFF7E], + "NumLock": [0xFF7F], + "PageDown": [0xFF56], + "PageUp": [0xFF55], + "Pause": [0xFF13], + "Play": [0xFD16], + "PreviousCandidate": [0xFF3E], + "PrintScreen": [0xFD1D], + "Redo": [0xFF66], + "Right": [0xFF53], + "RomanCharacters": null, + "Scroll": [0xFF14], + "Select": [0xFF60], + "Separator": [0xFFAC], + "Shift": [0xFFE1, 0xFFE1, 0xFFE2], + "SingleCandidate": [0xFF3C], + "Super": [0xFFEB, 0xFFEB, 0xFFEC], + "Tab": [0xFF09], + "Up": [0xFF52], + "Undo": [0xFF65], + "Win": [0xFFEB], + "Zenkaku": [0xFF28], + "ZenkakuHankaku": [0xFF2A] +}; \ No newline at end of file diff --git a/src/protocol.js b/src/protocol.js new file mode 100644 index 0000000..430a1a2 --- /dev/null +++ b/src/protocol.js @@ -0,0 +1,45 @@ +export const guacutils = { + decode: (string) => { + let pos = -1; + let sections = []; + + for(;;) { + let len = string.indexOf('.', pos + 1); + + if(len === -1) + break; + + pos = parseInt(string.slice(pos + 1, len)) + len + 1; + + // don't allow funky protocol length + if(pos > string.length) + return []; + + sections.push(string.slice(len + 1, pos)); + + + const sep = string.slice(pos, pos + 1); + + if(sep === ',') + continue; + else if(sep === ';') + break; + else + // Invalid data. + return []; + } + + return sections; + }, + + encode: (string) => { + let command = ''; + + for(var i = 0; i < string.length; i++) { + let current = string[i]; + command += current.toString().length + '.' + current; + command += ( i < string.length - 1 ? ',' : ';'); + } + return command; + } +}; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..3245091 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,9 @@ +const path = require("path"); +module.exports = { + entry: "./src/index.js", + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, + mode: "development" +} \ No newline at end of file