Implement opening the VM, as well as viewing the screen, chat, and userlist. It's pretty much fully functional as a view-only client, next up is interaction
This commit is contained in:
parent
225f91f7a4
commit
33d16f4c2f
|
|
@ -12,7 +12,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}*/
|
}*/
|
||||||
#display, #btns {
|
#vmDisplay, #btns {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -86,3 +86,11 @@
|
||||||
#forceVotePanel {
|
#forceVotePanel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-admin {
|
||||||
|
color: #FF0000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-moderator {
|
||||||
|
color: #00FF00 !important;
|
||||||
|
}
|
||||||
|
|
@ -117,9 +117,7 @@
|
||||||
<div class="row"></div>
|
<div class="row"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container-fluid" id="vmview">
|
<div class="container-fluid" id="vmview">
|
||||||
<div id="vmDisplay">
|
<div id="vmDisplay"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
<p id="turnstatus" class="text-light"></p>
|
<p id="turnstatus" class="text-light"></p>
|
||||||
<div id="voteResetPanel" class="bg-dark text-light" style="display:none;">
|
<div id="voteResetPanel" class="bg-dark text-light" style="display:none;">
|
||||||
Do you want to reset the vm?<br/>
|
Do you want to reset the vm?<br/>
|
||||||
|
|
@ -157,7 +155,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="table-responsive chat-table">
|
<div class="table-responsive chat-table" id="chatListDiv">
|
||||||
<table class="table table-hover table-dark table-borderless">
|
<table class="table table-hover table-dark table-borderless">
|
||||||
<tbody id="chatList">
|
<tbody id="chatList">
|
||||||
|
|
||||||
|
|
|
||||||
187
src/ts/main.ts
187
src/ts/main.ts
|
|
@ -1,13 +1,28 @@
|
||||||
import CollabVMClient from "./protocol/CollabVMClient.js";
|
import CollabVMClient from "./protocol/CollabVMClient.js";
|
||||||
import VM from "./protocol/VM.js";
|
import VM from "./protocol/VM.js";
|
||||||
import { Config } from "../../Config.js";
|
import { Config } from "../../Config.js";
|
||||||
|
import { Rank } from "./protocol/Permissions.js";
|
||||||
|
import { User } from "./protocol/User.js";
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
|
const w = window as any;
|
||||||
const elements = {
|
const elements = {
|
||||||
vmlist: document.getElementById('vmlist') as HTMLDivElement,
|
vmlist: document.getElementById('vmlist') as HTMLDivElement,
|
||||||
|
vmview: document.getElementById('vmview') as HTMLDivElement,
|
||||||
|
vmDisplay: document.getElementById('vmDisplay') as HTMLDivElement,
|
||||||
|
homeBtn: document.getElementById('homeBtn') as HTMLAnchorElement,
|
||||||
|
chatList: document.getElementById('chatList') as HTMLTableSectionElement,
|
||||||
|
chatListDiv: document.getElementById('chatListDiv') as HTMLDivElement,
|
||||||
|
userlist: document.getElementById('userlist') as HTMLTableSectionElement,
|
||||||
|
onlineusercount: document.getElementById("onlineusercount") as HTMLSpanElement,
|
||||||
|
username: document.getElementById("username") as HTMLSpanElement,
|
||||||
}
|
}
|
||||||
// Listed VMs
|
// Listed VMs
|
||||||
const vms : VM[] = [];
|
const vms : VM[] = [];
|
||||||
|
const cards : HTMLDivElement[] = [];
|
||||||
|
|
||||||
|
// Active VM
|
||||||
|
var VM : CollabVMClient | null = null;
|
||||||
|
|
||||||
function multicollab(url : string) {
|
function multicollab(url : string) {
|
||||||
return new Promise<void>(async (res, rej) => {
|
return new Promise<void>(async (res, rej) => {
|
||||||
|
|
@ -36,31 +51,181 @@ function multicollab(url : string) {
|
||||||
card.appendChild(vm.thumbnail);
|
card.appendChild(vm.thumbnail);
|
||||||
card.appendChild(cardBody);
|
card.appendChild(cardBody);
|
||||||
div.appendChild(card);
|
div.appendChild(card);
|
||||||
elements.vmlist.children[0].appendChild(div);
|
cards.push(div);
|
||||||
reloadVMList();
|
sortVMList();
|
||||||
}
|
}
|
||||||
res();
|
res();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function openVM(vm : VM) {
|
function openVM(vm : VM) {
|
||||||
|
return new Promise<void>(async (res, rej) => {
|
||||||
|
// If there's an active VM it must be closed before opening another
|
||||||
|
if (VM !== null) return;
|
||||||
|
// Set hash
|
||||||
|
location.hash = vm.id;
|
||||||
|
// Create the client
|
||||||
|
VM = new CollabVMClient(vm.url);
|
||||||
|
// Register event listeners
|
||||||
|
VM!.on('chat', (username, message) => chatMessage(username, message));
|
||||||
|
VM!.on('adduser', (user) => addUser(user));
|
||||||
|
VM!.on('remuser', (user) => remUser(user));
|
||||||
|
VM!.on('rename', (oldname, newname, selfrename) => userRenamed(oldname, newname, selfrename));
|
||||||
|
VM!.on('renamestatus', (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'taken': alert("That username is already taken"); break;
|
||||||
|
case 'invalid': alert("Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters."); break;
|
||||||
|
case 'blacklisted': alert("That username has been blacklisted."); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Wait for the client to open
|
||||||
|
await new Promise<void>(res => VM!.on('open', () => res()));
|
||||||
|
// Connect to node
|
||||||
|
chatMessage("", vm.id);
|
||||||
|
var connected = await VM.connect(vm.id);
|
||||||
|
if (!connected) {
|
||||||
|
VM.close();
|
||||||
|
VM = null;
|
||||||
|
rej("Failed to connect to node");
|
||||||
|
}
|
||||||
|
// Set the title
|
||||||
|
document.title = vm.id + " - CollabVM";
|
||||||
|
// Append canvas
|
||||||
|
elements.vmDisplay.appendChild(VM!.canvas);
|
||||||
|
// Switch to the VM view
|
||||||
|
elements.vmlist.style.display = "none";
|
||||||
|
elements.vmview.style.display = "block";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadVMList() {
|
function closeVM() {
|
||||||
var cards = Array.prototype.slice.call(elements.vmlist.children[0].children);
|
if (VM === null) return;
|
||||||
|
// Close the VM
|
||||||
|
VM.close();
|
||||||
|
VM = null;
|
||||||
|
// Remove the canvas
|
||||||
|
elements.vmDisplay.innerHTML = "";
|
||||||
|
// Switch to the VM list
|
||||||
|
elements.vmlist.style.display = "block";
|
||||||
|
elements.vmview.style.display = "none";
|
||||||
|
// Clear users
|
||||||
|
elements.userlist.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadList() {
|
||||||
|
return new Promise<void>(async res => {
|
||||||
|
var p = [];
|
||||||
|
for (var url of Config.ServerAddresses) {
|
||||||
|
p.push(multicollab(url));
|
||||||
|
}
|
||||||
|
await Promise.all(p);
|
||||||
|
var v = vms.find(v => v.id === window.location.hash.slice(1));
|
||||||
|
if (v !== undefined) openVM(v);
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortVMList() {
|
||||||
cards.sort(function(a, b) {
|
cards.sort(function(a, b) {
|
||||||
return a.id > b.id ? 1 : -1;
|
return a.children[0].getAttribute("data-cvm-node")! > b.id ? 1 : -1;
|
||||||
});
|
});
|
||||||
elements.vmlist.children[0].innerHTML = "";
|
elements.vmlist.children[0].innerHTML = "";
|
||||||
cards.forEach((c) => elements.vmlist.children[0].appendChild(c));
|
cards.forEach((c) => elements.vmlist.children[0].appendChild(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chatMessage(username : string, message : string) {
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
var td = document.createElement('td');
|
||||||
|
// System message
|
||||||
|
if (username === "") td.innerHTML = message;
|
||||||
|
else {
|
||||||
|
var user = VM!.getUsers().find(u => u.username === username);
|
||||||
|
var rank;
|
||||||
|
if (user !== undefined) rank = user.rank;
|
||||||
|
else rank = Rank.Unregistered;
|
||||||
|
var userclass;
|
||||||
|
var msgclass;
|
||||||
|
switch (rank) {
|
||||||
|
case Rank.Unregistered:
|
||||||
|
userclass = "user-unregistered";
|
||||||
|
msgclass = "chat-unregistered";
|
||||||
|
break;
|
||||||
|
case Rank.Admin:
|
||||||
|
userclass = "user-admin";
|
||||||
|
msgclass = "chat-admin";
|
||||||
|
break;
|
||||||
|
case Rank.Moderator:
|
||||||
|
userclass = "user-mod";
|
||||||
|
msgclass = "chat-moderator";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tr.classList.add(msgclass);
|
||||||
|
td.innerHTML = `<b class="${userclass}">${username}▸</b> ${message}`;
|
||||||
|
// hacky way to allow scripts
|
||||||
|
Array.prototype.slice.call(td.children).forEach((curr) => {
|
||||||
|
if (curr.nodeName === "SCRIPT") {
|
||||||
|
eval(curr.text)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tr.appendChild(td);
|
||||||
|
elements.chatList.appendChild(tr);
|
||||||
|
elements.chatListDiv.scrollTop = elements.chatListDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUser(user : User) {
|
||||||
|
var olduser = Array.prototype.slice.call(elements.userlist.children).find((u : HTMLTableRowElement) => u.children[0].innerHTML === user.username);
|
||||||
|
if (olduser !== undefined) elements.userlist.removeChild(olduser);
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
var td = document.createElement('td');
|
||||||
|
td.innerHTML = user.username;
|
||||||
|
switch (user.rank) {
|
||||||
|
case Rank.Admin:
|
||||||
|
td.classList.add("user-admin");
|
||||||
|
break;
|
||||||
|
case Rank.Moderator:
|
||||||
|
td.classList.add("user-moderator");
|
||||||
|
break;
|
||||||
|
case Rank.Unregistered:
|
||||||
|
td.classList.add("user-unregistered");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tr.appendChild(td);
|
||||||
|
elements.userlist.appendChild(tr);
|
||||||
|
elements.onlineusercount.innerHTML = VM!.getUsers().length.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function remUser(user : User) {
|
||||||
|
var olduser = Array.prototype.slice.call(elements.userlist.children).find((u : HTMLTableRowElement) => u.children[0].innerHTML === user.username);
|
||||||
|
if (olduser !== undefined) elements.userlist.removeChild(olduser);
|
||||||
|
elements.onlineusercount.innerHTML = VM!.getUsers().length.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function userRenamed(oldname : string, newname : string, selfrename : boolean) {
|
||||||
|
var user = Array.prototype.slice.call(elements.userlist.children).find((u : HTMLTableRowElement) => u.children[0].innerHTML === oldname);
|
||||||
|
if (user) {
|
||||||
|
user.children[0].innerHTML = newname;
|
||||||
|
}
|
||||||
|
if (selfrename) {
|
||||||
|
w.username = newname;
|
||||||
|
elements.username.innerText = newname;
|
||||||
|
localStorage.setItem("username", newname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind list buttons
|
||||||
|
elements.homeBtn.addEventListener('click', () => closeVM());
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
var w = window as any;
|
w.collabvm = {
|
||||||
|
openVM: openVM,
|
||||||
|
closeVM: closeVM,
|
||||||
|
loadList: loadList,
|
||||||
|
multicollab: multicollab
|
||||||
|
}
|
||||||
|
// Multicollab will stay in the global scope for backwards compatibility
|
||||||
w.multicollab = multicollab;
|
w.multicollab = multicollab;
|
||||||
w.openVM = openVM;
|
// Same goes for GetAdmin
|
||||||
|
// w.GetAdmin = () => VM.admin;
|
||||||
|
|
||||||
// Load all VMs
|
// Load all VMs
|
||||||
for (var url of Config.ServerAddresses) {
|
loadList();
|
||||||
multicollab(url);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import {createNanoEvents } from "nanoevents";
|
import {createNanoEvents } from "nanoevents";
|
||||||
import * as Guacutils from './Guacutils.js';
|
import * as Guacutils from './Guacutils.js';
|
||||||
import VM from "./VM.js";
|
import VM from "./VM.js";
|
||||||
|
import { User } from "./User.js";
|
||||||
|
import { Rank } from "./Permissions.js";
|
||||||
|
|
||||||
export default class CollabVMClient {
|
export default class CollabVMClient {
|
||||||
// Fields
|
// Fields
|
||||||
private socket : WebSocket;
|
private socket : WebSocket;
|
||||||
private canvas : HTMLCanvasElement;
|
canvas : HTMLCanvasElement;
|
||||||
private ctx : CanvasRenderingContext2D;
|
private ctx : CanvasRenderingContext2D;
|
||||||
private url : string;
|
private url : string;
|
||||||
|
private connectedToVM : boolean = false;
|
||||||
|
private users : User[] = [];
|
||||||
|
private username : string | null = null;
|
||||||
// events that are used internally and not exposed
|
// events that are used internally and not exposed
|
||||||
private emitter;
|
private emitter;
|
||||||
// public events
|
// public events
|
||||||
|
|
@ -56,6 +61,82 @@ export default class CollabVMClient {
|
||||||
// pass msgarr to the emitter for processing by list()
|
// pass msgarr to the emitter for processing by list()
|
||||||
console.log("got list")
|
console.log("got list")
|
||||||
this.emitter.emit('list', msgArr.slice(1));
|
this.emitter.emit('list', msgArr.slice(1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "connect": {
|
||||||
|
this.connectedToVM = msgArr[1] === "1";
|
||||||
|
this.emitter.emit('connect', this.connectedToVM);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "size": {
|
||||||
|
if (msgArr[1] !== "0") return;
|
||||||
|
this.canvas.width = parseInt(msgArr[2]);
|
||||||
|
this.canvas.height = parseInt(msgArr[3]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "png": {
|
||||||
|
// Despite the opcode name, this is actually JPEG, because old versions of the server used PNG and yknow backwards compatibility
|
||||||
|
var img = new Image();
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
this.ctx.drawImage(img, parseInt(msgArr[3]), parseInt(msgArr[4]));
|
||||||
|
});
|
||||||
|
img.src = "data:image/jpeg;base64," + msgArr[5];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "chat": {
|
||||||
|
for (var i = 1; i < msgArr.length; i += 2) {
|
||||||
|
this.publicEmitter.emit('chat', msgArr[i], msgArr[i + 1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "adduser": {
|
||||||
|
for (var i = 2; i < msgArr.length; i += 2) {
|
||||||
|
var user = new User(msgArr[i], parseInt(msgArr[i + 1]) as Rank);
|
||||||
|
this.users.push(user);
|
||||||
|
this.publicEmitter.emit('adduser', user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "remuser": {
|
||||||
|
for (var i = 2; i < msgArr.length; i++) {
|
||||||
|
var _user = this.users.find(u => u.username === msgArr[i]);
|
||||||
|
if (_user === undefined) continue;
|
||||||
|
this.users.splice(this.users.indexOf(_user), 1);
|
||||||
|
this.publicEmitter.emit('remuser', _user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "rename": {
|
||||||
|
var selfrename = false;
|
||||||
|
var oldusername : string | null = null;
|
||||||
|
// We've been renamed
|
||||||
|
if (msgArr[1] === "0") {
|
||||||
|
selfrename = true;
|
||||||
|
oldusername = this.username;
|
||||||
|
// msgArr[2] is the status of the rename
|
||||||
|
// Anything other than 0 is an error, however the server will still rename us to a guest name
|
||||||
|
switch (msgArr[2]) {
|
||||||
|
case "1":
|
||||||
|
// The username we wanted was taken
|
||||||
|
this.publicEmitter.emit('renamestatus', 'taken');
|
||||||
|
break;
|
||||||
|
case "2":
|
||||||
|
// The username we wanted was invalid
|
||||||
|
this.publicEmitter.emit('renamestatus', 'invalid');
|
||||||
|
break;
|
||||||
|
case "3":
|
||||||
|
// The username we wanted is blacklisted
|
||||||
|
this.publicEmitter.emit('renamestatus', 'blacklisted');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.username = msgArr[3];
|
||||||
|
}
|
||||||
|
else oldusername = msgArr[2];
|
||||||
|
var _user = this.users.find(u => u.username === oldusername);
|
||||||
|
if (_user) {
|
||||||
|
_user.username = msgArr[3];
|
||||||
|
}
|
||||||
|
this.publicEmitter.emit('rename', oldusername, msgArr[3], selfrename);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,5 +169,30 @@ export default class CollabVMClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connect to a node
|
||||||
|
connect(id : string, username : string | null = null) : Promise<boolean> {
|
||||||
|
return new Promise(res => {
|
||||||
|
var u = this.emitter.on('connect', (success : boolean) => {
|
||||||
|
u();
|
||||||
|
res(success);
|
||||||
|
});
|
||||||
|
if (username === null) this.send("rename");
|
||||||
|
else this.send("rename", username);
|
||||||
|
this.send("connect", id);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the connection
|
||||||
|
close() {
|
||||||
|
this.connectedToVM = false;
|
||||||
|
this.socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users
|
||||||
|
getUsers() : User[] {
|
||||||
|
// Return a copy of the array
|
||||||
|
return this.users.slice();
|
||||||
|
}
|
||||||
|
|
||||||
on = (event : string | number, cb: (...args: any) => void) => this.publicEmitter.on(event, cb);
|
on = (event : string | number, cb: (...args: any) => void) => this.publicEmitter.on(event, cb);
|
||||||
}
|
}
|
||||||
31
src/ts/protocol/Permissions.ts
Normal file
31
src/ts/protocol/Permissions.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export class Permissions {
|
||||||
|
restore : boolean = false;
|
||||||
|
reboot : boolean = false;
|
||||||
|
ban : boolean = false;
|
||||||
|
forcevote : boolean = false;
|
||||||
|
mute : boolean = false;
|
||||||
|
kick : boolean = false;
|
||||||
|
bypassturn : boolean = false;
|
||||||
|
rename : boolean = false;
|
||||||
|
grabip : boolean = false;
|
||||||
|
xss : boolean = false;
|
||||||
|
|
||||||
|
constructor(mask : number) {
|
||||||
|
this.restore = (mask & 1) !== 0;
|
||||||
|
this.reboot = (mask & 2) !== 0;
|
||||||
|
this.ban = (mask & 4) !== 0;
|
||||||
|
this.forcevote = (mask & 8) !== 0;
|
||||||
|
this.mute = (mask & 16) !== 0;
|
||||||
|
this.kick = (mask & 32) !== 0;
|
||||||
|
this.bypassturn = (mask & 64) !== 0;
|
||||||
|
this.rename = (mask & 128) !== 0;
|
||||||
|
this.grabip = (mask & 256) !== 0;
|
||||||
|
this.xss = (mask & 512) !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Rank {
|
||||||
|
Unregistered = 0,
|
||||||
|
Admin = 2,
|
||||||
|
Moderator = 3,
|
||||||
|
}
|
||||||
11
src/ts/protocol/User.ts
Normal file
11
src/ts/protocol/User.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Rank } from "./Permissions.js";
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
username : string;
|
||||||
|
rank : Rank
|
||||||
|
|
||||||
|
constructor(username : string, rank : Rank = Rank.Unregistered) {
|
||||||
|
this.username = username;
|
||||||
|
this.rank = rank;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user