diff --git a/dist/index.html b/dist/index.html
index 0f2a9ee..b43136d 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -55,6 +55,13 @@
+
diff --git a/dist/style.css b/dist/style.css
index 24c43e4..7bf4bd2 100644
--- a/dist/style.css
+++ b/dist/style.css
@@ -19,6 +19,7 @@
display: block;
margin-bottom: 10px;
}
+
#vmlist > div.row > div {
padding-bottom: 10px;
}
@@ -26,9 +27,11 @@
cursor: pointer;
border-color: rgb(8, 121, 250);
}
+
.vmtile > img {
margin-bottom: 2px;
-}*/
+}
+
.chat-table, .username-table {
overflow-y: auto;
border: 1px solid #575757;
@@ -36,6 +39,7 @@
.chat-table {
height: 30vh;
}
+
.username-table {
max-height: 30vh;
}
@@ -43,9 +47,29 @@
position: sticky;
top: 0;
}
+
#turnstatus {
text-align: center;
}
#voteResetPanel {
text-align: center;
+}
+
+.focused {
+ box-shadow: 0 0 9px 0 rgba(45,213,255,.75);
+ -moz-box-shadow: 0 0 9px 0 rgba(45,213,255,.75);
+ -webkit-box-shadow: 0 0 9px 0 rgba(45,213,255,.75)
+}
+
+.waiting {
+ box-shadow: 0 0 9px 0 rgba(242,255,63,.75);
+ -moz-box-shadow: 0 0 9px 0 rgba(242,255,63,.75);
+ -webkit-box-shadow: 0 0 9px 0 rgba(242,255,63,.75)
+}
+
+#staffbtns {
+ display: none;
+}
+#staffbtns > button {
+ display: none;
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 26545db..d69059a 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"author": "Elijah R",
"license": "GPL-3.0",
"dependencies": {
+ "nanoevents": "^7.0.1",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
}
diff --git a/src/index.js b/src/index.js
index 84a5fa9..4aa53af 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,11 +1,14 @@
import { guacutils } from "./protocol";
import { config } from "./common";
import { GetKeysym } from "./keyboard";
+import { createNanoEvents } from "nanoevents";
+import { makeperms } from "./permissions";
// None = -1
// Has turn = 0
// In queue =
var turn = -1;
-var perms = 0;
+var perms = makeperms(0);
+var rank = 0;
var connected = false;
const vms = [];
const users = [];
@@ -13,7 +16,13 @@ const buttons = {
takeTurn: window.document.getElementById("takeTurnBtn"),
changeUsername: window.document.getElementById("changeUsernameBtn"),
voteReset: window.document.getElementById("voteResetButton"),
- screenshot: window.document.getElementById("screenshotButton")
+ screenshot: window.document.getElementById("screenshotButton"),
+ // Staff
+ restore: window.document.getElementById("restoreBtn"),
+ reboot: window.document.getElementById("rebootBtn"),
+ clearQueue: window.document.getElementById("clearQueueBtn"),
+ bypassTurn: window.document.getElementById("bypassTurnBtn"),
+ endTurn: window.document.getElementById("endTurnBtn"),
}
var hasTurn = false;
var vm;
@@ -37,11 +46,14 @@ const votenobtn = document.getElementById("voteNoBtn");
const voteyeslabel = document.getElementById("voteYesLabel");
const votenolabel = document.getElementById("voteNoLabel");
const votetime = document.getElementById("votetime");
+const staffbtns = document.getElementById("staffbtns");
// needed to scroll to bottom
const chatListDiv = document.querySelector(".chat-table");
class CollabVMClient {
+ eventemitter = createNanoEvents();
socket;
+ node;
#url;
constructor(url) {
this.#url = url;
@@ -63,26 +75,23 @@ class CollabVMClient {
}
connectToVM(node) {
return new Promise((res, rej) => {
+ this.node = node;
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);
+ var unbind = this.eventemitter.on('connect', () => {
+ unbind();
+ res();
+ });
+ var failunbind = this.eventemitter.on('connectfail', () => {
+ failunbind();
+ rej();
+ });
this.socket.send(guacutils.encode(["connect", node]));
+ var pass = window.localStorage.getItem("password_"+this.#url);
+ if (pass)
+ this.admin.login(pass);
});
}
async #onMessage(event) {
@@ -91,6 +100,16 @@ class CollabVMClient {
case "nop":
this.socket.send("3.nop;");
break;
+ case "connect":
+ switch (msgArr[1]) {
+ case "0":
+ this.eventemitter.emit('connectfail');
+ break;
+ case "1":
+ this.eventemitter.emit('connect');
+ break;
+ }
+ break;
case "chat":
if (!connected) return;
for (var i = 1; i < msgArr.length; i += 2) {
@@ -99,6 +118,19 @@ class CollabVMClient {
chatsound.play();
chatListDiv.scrollTop = chatListDiv.scrollHeight;
break;
+ case "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.eventemitter.emit('list', list);
+ break;
case "size":
if (!connected || msgArr[1] !== "0") return;
display.width = msgArr[2];
@@ -171,6 +203,7 @@ class CollabVMClient {
buttons.takeTurn.innerText = "Take Turn";
turn = -1;
turnstatus.innerText = "";
+ display.className = "";
// Get the number of users queued for a turn
var queuedUsers = Number(msgArr[2]);
if (queuedUsers === 0) return;
@@ -182,13 +215,15 @@ class CollabVMClient {
if (currentTurnUsername === window.username) {
turn = 0;
turnstatus.innerText = "You have the turn.";
- }
+ display.className = "focused";
+ }
// 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";
+ display.className = "waiting";
};
var user = users.find(u => u.username === msgArr[i+3]);
user.turn = i;
@@ -234,6 +269,50 @@ class CollabVMClient {
break;
}
break;
+ case "admin":
+ switch (msgArr[1]) {
+ case "0":
+ // Login
+ switch (msgArr[2]) {
+ case "0":
+ this.eventemitter.emit('login', {error: 'badpassword'});
+ return;
+ break;
+ case "1":
+ perms = makeperms(65535);
+ rank = 2;
+ break;
+ case "3":
+ rank = 3;
+ perms = makeperms(parseInt(msgArr[3]))
+ }
+ this.eventemitter.emit('login', {perms: perms, rank: rank});
+ usernameSpan.classList.remove("text-light");
+ switch (rank) {
+ case 2:
+ usernameSpan.classList.add("text-danger");
+ break;
+ case 3:
+ usernameSpan.classList.add("text-success");
+ break;
+ }
+ // Disabled for now until we figure out the issue of uservm
+ //window.localStorage.setItem("password_"+this.#url, password);
+ staffbtns.style.display = "block";
+ if (perms.restore) buttons.restore.style.display = "inline-block";
+ if (perms.reboot) buttons.reboot.style.display = "inline-block";
+ if (perms.bypassturn) {
+ buttons.bypassTurn.style.display = "inline-block";
+ buttons.clearQueue.style.display = "inline-block";
+ buttons.endTurn.style.display = "inline-block";
+ }
+ break;
+
+ }
+ break;
+ default:
+ window.cvmEvents.emit(msgArr[0], msgArr.slice(1));
+ break;
}
}
reloadUsers() {
@@ -252,24 +331,10 @@ class CollabVMClient {
}
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);
+ var unbind = this.eventemitter.on('list', (e) => {
+ unbind();
+ res(e);
+ })
this.socket.send("4.list;");
});
}
@@ -309,6 +374,28 @@ class CollabVMClient {
voteReset(reset) {
this.socket.send(guacutils.encode(["vote", reset ? "1" : "0"]));
}
+ admin = {
+ login: (password) => {
+ return new Promise((res, rej) => {
+ var unbind = this.eventemitter.on('login', (args) => {
+ unbind();
+ if (args.error) rej(error);
+ res(args);
+ })
+ this.socket.send(guacutils.encode(["admin", "2", password]));
+ });
+ },
+ adminInstruction: (...args) => { // Compatibility
+ args.unshift("admin");
+ console.log(args);
+ this.socket.send(guacutils.encode(args));
+ },
+ restore: () => this.socket.send(guacutils.encode(["admin", "8", this.node])),
+ reboot: () => this.socket.send(guacutils.encode(["admin", "10", this.node])),
+ clearQueue: () => this.socket.send(guacutils.encode(["admin", "17", this.node])),
+ bypassTurn: () => this.socket.send(guacutils.encode(["admin", "20"])),
+ endTurn: () => this.socket.send(guacutils.encode(["admin", "16", users[0].username])),
+ }
}
function multicollab(url) {
return new Promise(async (res, rej) => {
@@ -364,15 +451,15 @@ async function openVM(url, node) {
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('mousemove', (e) => vm.mouseevent(e), {capture: true})
+ display.addEventListener('mousedown', (e) => vm.mouseevent(e), {capture: true});
+ display.addEventListener('mouseup', (e) => vm.mouseevent(e), {capture: true});
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));
+ }, {capture: true});
+ display.addEventListener('keydown', (e) => vm.keyevent(e, true), {capture: true});
+ display.addEventListener('keyup', (e) => vm.keyevent(e, false), {capture: true});
}
function screenshotVM() {
return new Promise((res, rej) => {
@@ -406,9 +493,31 @@ buttons.takeTurn.addEventListener('click', () => vm.turn());
buttons.voteReset.addEventListener('click', () => vm.voteReset(true));
voteyesbtn.addEventListener('click', () => vm.voteReset(true));
votenobtn.addEventListener('click', () => vm.voteReset(false));
+// Staff buttons
+buttons.restore.addEventListener('click', () => vm.admin.restore());
+buttons.reboot.addEventListener('click', () => vm.admin.reboot());
+buttons.clearQueue.addEventListener('click', () => vm.admin.clearQueue());
+buttons.bypassTurn.addEventListener('click', () => vm.admin.bypassTurn());
+buttons.endTurn.addEventListener('click', () => vm.admin.endTurn());
+// Login
+var usernameClick = false;
+usernameSpan.addEventListener('click', () => {
+ if (!usernameClick) {
+ usernameClick = true;
+ setInterval(() => {usernameClick = false;}, 1000);
+ return;
+ }
+ var pass = window.prompt("🔑");
+ if (!pass) return;
+ vm.admin.login(pass);
+});
// Load all vms
config.serverAddresses.forEach(multicollab);
// Export some stuff
window.screenshotVM = screenshotVM;
-window.multicollab = multicollab;
\ No newline at end of file
+window.multicollab = multicollab;
+window.getPerms = () => perms;
+window.getRank = () => rank;
+window.GetAdmin = () => vm.admin;
+window.cvmEvents = createNanoEvents();
\ No newline at end of file
diff --git a/src/permissions.js b/src/permissions.js
new file mode 100644
index 0000000..87dca86
--- /dev/null
+++ b/src/permissions.js
@@ -0,0 +1,23 @@
+export function makeperms(mask) {
+ const perms = {
+ restore: false,
+ reboot: false,
+ ban: false,
+ forcevote: false,
+ mute: false,
+ kick: false,
+ bypassturn: false,
+ rename: false,
+ grabip: false
+ };
+ if ((mask & 1) !== 0) perms.restore = true;
+ if ((mask & 2) !== 0) perms.reboot = true;
+ if ((mask & 4) !== 0) perms.ban = true;
+ if ((mask & 8) !== 0) perms.forcevote = true;
+ if ((mask & 16) !== 0) perms.mute = true;
+ if ((mask & 32) !== 0) perms.kick = true;
+ if ((mask & 64) !== 0) perms.bypassturn = true;
+ if ((mask & 128) !== 0) perms.rename = true;
+ if ((mask & 256) !== 0) perms.grabip = true;
+ return perms;
+}
\ No newline at end of file