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;
|
||||
padding: 4px;
|
||||
}*/
|
||||
#display, #btns {
|
||||
#vmDisplay, #btns {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
|
|
@ -85,4 +85,12 @@
|
|||
|
||||
#forceVotePanel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-admin {
|
||||
color: #FF0000 !important;
|
||||
}
|
||||
|
||||
.user-moderator {
|
||||
color: #00FF00 !important;
|
||||
}
|
||||
|
|
@ -117,9 +117,7 @@
|
|||
<div class="row"></div>
|
||||
</div>
|
||||
<div class="container-fluid" id="vmview">
|
||||
<div id="vmDisplay">
|
||||
|
||||
</div>
|
||||
<div id="vmDisplay"></div>
|
||||
<p id="turnstatus" class="text-light"></p>
|
||||
<div id="voteResetPanel" class="bg-dark text-light" style="display:none;">
|
||||
Do you want to reset the vm?<br/>
|
||||
|
|
@ -157,7 +155,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<tbody id="chatList">
|
||||
|
||||
|
|
|
|||
187
src/ts/main.ts
187
src/ts/main.ts
|
|
@ -1,13 +1,28 @@
|
|||
import CollabVMClient from "./protocol/CollabVMClient.js";
|
||||
import VM from "./protocol/VM.js";
|
||||
import { Config } from "../../Config.js";
|
||||
import { Rank } from "./protocol/Permissions.js";
|
||||
import { User } from "./protocol/User.js";
|
||||
|
||||
// Elements
|
||||
const w = window as any;
|
||||
const elements = {
|
||||
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
|
||||
const vms : VM[] = [];
|
||||
const cards : HTMLDivElement[] = [];
|
||||
|
||||
// Active VM
|
||||
var VM : CollabVMClient | null = null;
|
||||
|
||||
function multicollab(url : string) {
|
||||
return new Promise<void>(async (res, rej) => {
|
||||
|
|
@ -36,31 +51,181 @@ function multicollab(url : string) {
|
|||
card.appendChild(vm.thumbnail);
|
||||
card.appendChild(cardBody);
|
||||
div.appendChild(card);
|
||||
elements.vmlist.children[0].appendChild(div);
|
||||
reloadVMList();
|
||||
cards.push(div);
|
||||
sortVMList();
|
||||
}
|
||||
res();
|
||||
});
|
||||
}
|
||||
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() {
|
||||
var cards = Array.prototype.slice.call(elements.vmlist.children[0].children);
|
||||
function closeVM() {
|
||||
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) {
|
||||
return a.id > b.id ? 1 : -1;
|
||||
return a.children[0].getAttribute("data-cvm-node")! > b.id ? 1 : -1;
|
||||
});
|
||||
elements.vmlist.children[0].innerHTML = "";
|
||||
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
|
||||
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.openVM = openVM;
|
||||
// Same goes for GetAdmin
|
||||
// w.GetAdmin = () => VM.admin;
|
||||
|
||||
// Load all VMs
|
||||
for (var url of Config.ServerAddresses) {
|
||||
multicollab(url);
|
||||
}
|
||||
loadList();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import {createNanoEvents } from "nanoevents";
|
||||
import * as Guacutils from './Guacutils.js';
|
||||
import VM from "./VM.js";
|
||||
import { User } from "./User.js";
|
||||
import { Rank } from "./Permissions.js";
|
||||
|
||||
export default class CollabVMClient {
|
||||
// Fields
|
||||
private socket : WebSocket;
|
||||
private canvas : HTMLCanvasElement;
|
||||
canvas : HTMLCanvasElement;
|
||||
private ctx : CanvasRenderingContext2D;
|
||||
private url : string;
|
||||
private connectedToVM : boolean = false;
|
||||
private users : User[] = [];
|
||||
private username : string | null = null;
|
||||
// events that are used internally and not exposed
|
||||
private emitter;
|
||||
// public events
|
||||
|
|
@ -56,6 +61,82 @@ export default class CollabVMClient {
|
|||
// pass msgarr to the emitter for processing by list()
|
||||
console.log("got list")
|
||||
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);
|
||||
}
|
||||
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