webapp/src/ts/main.ts
2024-06-23 02:18:35 -04:00

1371 lines
55 KiB
TypeScript

import CollabVMClient from './protocol/CollabVMClient.js';
import VM from './protocol/VM.js';
import Config from '../../config.json';
import { Permissions, Rank } from './protocol/Permissions.js';
import { User } from './protocol/User.js';
import TurnStatus from './protocol/TurnStatus.js';
import Keyboard from 'simple-keyboard';
import { OSK_buttonToKeysym } from './keyboard';
import 'simple-keyboard/build/css/index.css';
import VoteStatus from './protocol/VoteStatus.js';
import * as bootstrap from 'bootstrap';
import MuteState from './protocol/MuteState.js';
import { Unsubscribe } from 'nanoevents';
import { I18nStringKey, TheI18n } from './i18n.js';
import { Format } from './format.js';
import AuthManager from './AuthManager.js';
import dayjs from 'dayjs';
import * as dompurify from 'dompurify';
// 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,
rulesBtn: document.getElementById('rulesBtn') 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,
chatinput: document.getElementById('chat-input') as HTMLInputElement,
sendChatBtn: document.getElementById('sendChatBtn') as HTMLButtonElement,
takeTurnBtn: document.getElementById('takeTurnBtn') as HTMLButtonElement,
changeUsernameBtn: document.getElementById('changeUsernameBtn') as HTMLButtonElement,
turnBtnText: document.getElementById('turnBtnText') as HTMLSpanElement,
turnstatus: document.getElementById('turnstatus') as HTMLParagraphElement,
osk: window.document.getElementById('oskBtn') as HTMLButtonElement,
oskContainer: document.getElementById('osk-container') as HTMLDivElement,
screenshotButton: document.getElementById('screenshotButton') as HTMLButtonElement,
voteResetButton: document.getElementById('voteResetButton') as HTMLButtonElement,
voteResetPanel: document.getElementById('voteResetPanel') as HTMLDivElement,
voteYesBtn: document.getElementById('voteYesBtn') as HTMLButtonElement,
voteNoBtn: document.getElementById('voteNoBtn') as HTMLButtonElement,
voteYesLabel: document.getElementById('voteYesLabel') as HTMLSpanElement,
voteNoLabel: document.getElementById('voteNoLabel') as HTMLSpanElement,
voteTimeText: document.getElementById('voteTimeText') as HTMLSpanElement,
loginModal: document.getElementById('loginModal') as HTMLDivElement,
adminPassword: document.getElementById('adminPassword') as HTMLInputElement,
loginButton: document.getElementById('loginButton') as HTMLButtonElement,
adminInputVMID: document.getElementById('adminInputVMID') as HTMLInputElement,
badPasswordAlert: document.getElementById('badPasswordAlert') as HTMLDivElement,
incorrectPasswordDismissBtn: document.getElementById('incorrectPasswordDismissBtn') as HTMLButtonElement,
ctrlAltDelBtn: document.getElementById('ctrlAltDelBtn') as HTMLButtonElement,
toggleThemeBtn: document.getElementById('toggleThemeBtn') as HTMLAnchorElement,
toggleThemeIcon: document.getElementById('toggleThemeIcon') as HTMLElement,
toggleThemeBtnText: document.getElementById('toggleThemeBtnText') as HTMLSpanElement,
// Admin
staffbtns: document.getElementById('staffbtns') as HTMLDivElement,
restoreBtn: document.getElementById('restoreBtn') as HTMLButtonElement,
rebootBtn: document.getElementById('rebootBtn') as HTMLButtonElement,
clearQueueBtn: document.getElementById('clearQueueBtn') as HTMLButtonElement,
bypassTurnBtn: document.getElementById('bypassTurnBtn') as HTMLButtonElement,
endTurnBtn: document.getElementById('endTurnBtn') as HTMLButtonElement,
qemuMonitorBtn: document.getElementById('qemuMonitorBtn') as HTMLButtonElement,
xssCheckboxContainer: document.getElementById('xssCheckboxContainer') as HTMLDivElement,
xssCheckbox: document.getElementById('xssCheckbox') as HTMLInputElement,
forceVotePanel: document.getElementById('forceVotePanel') as HTMLDivElement,
forceVoteYesBtn: document.getElementById('forceVoteYesBtn') as HTMLButtonElement,
forceVoteNoBtn: document.getElementById('forceVoteNoBtn') as HTMLButtonElement,
indefTurnBtn: document.getElementById('indefTurnBtn') as HTMLButtonElement,
ghostTurnBtn: document.getElementById('ghostTurnBtn') as HTMLButtonElement,
ghostTurnBtnText: document.getElementById('ghostTurnBtnText') as HTMLSpanElement,
qemuMonitorInput: document.getElementById('qemuMonitorInput') as HTMLInputElement,
qemuMonitorSendBtn: document.getElementById('qemuMonitorSendBtn') as HTMLButtonElement,
qemuMonitorOutput: document.getElementById('qemuMonitorOutput') as HTMLTextAreaElement,
// Auth
accountDropdownUsername: document.getElementById("accountDropdownUsername") as HTMLSpanElement,
accountDropdownMenuLink: document.getElementById("accountDropdownMenuLink") as HTMLDivElement,
accountLoginButton: document.getElementById("accountLoginButton") as HTMLAnchorElement,
accountRegisterButton: document.getElementById("accountRegisterButton") as HTMLAnchorElement,
accountSettingsButton: document.getElementById("accountSettingsButton") as HTMLAnchorElement,
accountLogoutButton: document.getElementById("accountLogoutButton") as HTMLAnchorElement,
accountModal: document.getElementById("accountModal") as HTMLDivElement,
accountModalError: document.getElementById("accountModalError") as HTMLDivElement,
accountModalErrorText: document.getElementById("accountModalErrorText") as HTMLSpanElement,
accountModalErrorDismiss: document.getElementById("accountModalErrorDismiss") as HTMLButtonElement,
accountModalSuccess: document.getElementById("accountModalSuccess") as HTMLDivElement,
accountModalSuccessText: document.getElementById("accountModalSuccessText") as HTMLSpanElement,
accountModalSuccessDismiss: document.getElementById("accountModalSuccessDismiss") as HTMLButtonElement,
accountLoginSection: document.getElementById("accountLoginSection") as HTMLDivElement,
accountRegisterSection: document.getElementById("accountRegisterSection") as HTMLDivElement,
accountVerifyEmailSection: document.getElementById("accountVerifyEmailSection") as HTMLDivElement,
accountVerifyEmailText: document.getElementById("accountVerifyEmailText") as HTMLParagraphElement,
accountModalTitle: document.getElementById("accountModalTitle") as HTMLHeadingElement,
accountLoginForm: document.getElementById("accountLoginForm") as HTMLFormElement,
accountRegisterForm: document.getElementById("accountRegisterForm") as HTMLFormElement,
accountVerifyEmailForm: document.getElementById("accountVerifyEmailForm") as HTMLFormElement,
accountLoginCaptcha: document.getElementById("accountLoginCaptcha") as HTMLDivElement,
accountRegisterCaptcha: document.getElementById("accountRegisterCaptcha") as HTMLDivElement,
accountLoginUsername: document.getElementById("accountLoginUsername") as HTMLInputElement,
accountLoginPassword: document.getElementById("accountLoginPassword") as HTMLInputElement,
accountRegisterEmail: document.getElementById("accountRegisterEmail") as HTMLInputElement,
accountRegisterUsername: document.getElementById("accountRegisterUsername") as HTMLInputElement,
accountRegisterPassword: document.getElementById("accountRegisterPassword") as HTMLInputElement,
accountRegisterConfirmPassword: document.getElementById("accountRegisterConfirmPassword") as HTMLInputElement,
accountRegisterDateOfBirth: document.getElementById("accountRegisterDateOfBirth") as HTMLInputElement,
accountVerifyEmailCode: document.getElementById("accountVerifyEmailCode") as HTMLInputElement,
accountVerifyEmailPassword: document.getElementById("accountVerifyEmailPassword") as HTMLInputElement,
accountSettingsSection: document.getElementById("accountSettingsSection") as HTMLDivElement,
accountSettingsForm: document.getElementById("accountSettingsForm") as HTMLFormElement,
accountSettingsEmail: document.getElementById("accountSettingsEmail") as HTMLInputElement,
accountSettingsUsername: document.getElementById("accountSettingsUsername") as HTMLInputElement,
accountSettingsNewPassword: document.getElementById("accountSettingsNewPassword") as HTMLInputElement,
accountSettingsConfirmNewPassword: document.getElementById("accountSettingsConfirmNewPassword") as HTMLInputElement,
accountSettingsCurrentPassword: document.getElementById("accountSettingsCurrentPassword") as HTMLInputElement,
hideFlagCheckbox: document.getElementById("hideFlagCheckbox") as HTMLInputElement,
accountResetPasswordSection: document.getElementById("accountResetPasswordSection") as HTMLDivElement,
accountResetPasswordForm: document.getElementById("accountResetPasswordForm") as HTMLFormElement,
accountResetPasswordEmail: document.getElementById("accountResetPasswordEmail") as HTMLInputElement,
accountResetPasswordUsername: document.getElementById("accountResetPasswordUsername") as HTMLInputElement,
accountResetPasswordCaptcha: document.getElementById("accountResetPasswordCaptcha") as HTMLDivElement,
accountResetPasswordVerifySection: document.getElementById("accountResetPasswordVerifySection") as HTMLDivElement,
accountVerifyPasswordResetText: document.getElementById("accountVerifyPasswordResetText") as HTMLParagraphElement,
accountResetPasswordVerifyForm: document.getElementById("accountResetPasswordVerifyForm") as HTMLFormElement,
accountResetPasswordCode: document.getElementById("accountResetPasswordCode") as HTMLInputElement,
accountResetPasswordNewPassword: document.getElementById("accountResetPasswordNewPassword") as HTMLInputElement,
accountResetPasswordConfirmNewPassword: document.getElementById("accountResetPasswordConfirmNewPassword") as HTMLInputElement,
accountForgotPasswordButton: document.getElementById("accountForgotPasswordButton") as HTMLButtonElement,
};
let auth : AuthManager|null = null;
/* Start OSK */
let commonKeyboardOptions = {
onKeyPress: (button: string) => onKeyPress(button),
theme: 'simple-keyboard hg-theme-default cvmDark cvmDisabled hg-layout-default',
syncInstanceInputs: true,
mergeDisplay: true
};
let keyboard = new Keyboard('.osk-main', {
...commonKeyboardOptions,
layout: {
default: [
'{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}',
'` 1 2 3 4 5 6 7 8 9 0 - = {backspace}',
'{tab} q w e r t y u i o p [ ] \\',
"{capslock} a s d f g h j k l ; ' {enter}",
'{shiftleft} z x c v b n m , . / {shiftright}',
'{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}'
],
shift: [
'{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}',
'~ ! @ # $ % ^ & * ( ) _ + {backspace}',
'{tab} Q W E R T Y U I O P { } |',
'{capslock} A S D F G H J K L : " {enter}',
'{shiftleft} Z X C V B N M < > ? {shiftright}',
'{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}'
],
capslock: [
'{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}',
'` 1 2 3 4 5 6 7 8 9 0 - = {backspace}',
'{tab} Q W E R T Y U I O P [ ] \\',
"{capslock} A S D F G H J K L ; ' {enter}",
'{shiftleft} Z X C V B N M , . / {shiftright}',
'{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}'
],
shiftcaps: [
'{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}',
'~ ! @ # $ % ^ & * ( ) _ + {backspace}',
'{tab} q w e r t y u i o p { } |',
'{capslock} a s d f g h j k l : " {enter}',
'{shiftleft} z x c v b n m < > ? {shiftright}',
'{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}'
]
},
display: {
'{escape}': 'Esc',
'{tab}': 'Tab',
'{backspace}': 'Back',
'{enter}': 'Enter',
'{capslock}': 'Caps',
'{shiftleft}': 'Shift',
'{shiftright}': 'Shift',
'{controlleft}': 'Ctrl',
'{controlright}': 'Ctrl',
'{altleft}': 'Alt',
'{altright}': 'Alt',
'{metaleft}': 'Super',
'{metaright}': 'Menu'
}
});
let keyboardControlPad = new Keyboard('.osk-control', {
...commonKeyboardOptions,
layout: {
default: ['{prtscr} {scrolllock} {pause}', '{insert} {home} {pageup}', '{delete} {end} {pagedown}']
},
display: {
'{prtscr}': 'Print',
'{scrolllock}': 'Scroll',
'{pause}': 'Pause',
'{insert}': 'Ins',
'{home}': 'Home',
'{pageup}': 'PgUp',
'{delete}': 'Del',
'{end}': 'End',
'{pagedown}': 'PgDn'
}
});
let keyboardArrows = new Keyboard('.osk-arrows', {
...commonKeyboardOptions,
layout: {
default: ['{arrowup}', '{arrowleft} {arrowdown} {arrowright}']
}
});
let keyboardNumPad = new Keyboard('.osk-numpad', {
...commonKeyboardOptions,
layout: {
default: ['{numlock} {numpaddivide} {numpadmultiply}', '{numpad7} {numpad8} {numpad9}', '{numpad4} {numpad5} {numpad6}', '{numpad1} {numpad2} {numpad3}', '{numpad0} {numpaddecimal}']
}
});
let keyboardNumPadEnd = new Keyboard('.osk-numpadEnd', {
...commonKeyboardOptions,
layout: {
default: ['{numpadsubtract}', '{numpadadd}', '{numpadenter}']
}
});
let shiftHeld = false;
let ctrlHeld = false;
let capsHeld = false;
let altHeld = false;
let metaHeld = false;
const setButtonBackground = (selectors: string, condition: boolean) => {
for (let button of document.querySelectorAll(selectors) as NodeListOf<HTMLDivElement>) {
button.style.backgroundColor = condition ? '#1c4995' : 'rgba(0, 0, 0, 0.5)';
}
};
const enableOSK = (enable: boolean) => {
const theme = `simple-keyboard hg-theme-default cvmDark ${enable ? '' : 'cvmDisabled'} hg-layout-default`;
[keyboard, keyboardControlPad, keyboardArrows, keyboardNumPad, keyboardNumPadEnd].forEach((part) => {
part.setOptions({
theme: theme
});
});
if (enable) updateOSKStyle();
};
const updateOSKStyle = () => {
setButtonBackground('.hg-button-shiftleft, .hg-button-shiftright', shiftHeld);
setButtonBackground('.hg-button-controlleft, .hg-button-controlright', ctrlHeld);
setButtonBackground('.hg-button-capslock', capsHeld);
setButtonBackground('.hg-button-altleft, .hg-button-altright', altHeld);
setButtonBackground('.hg-button-metaleft, .hg-button-metaright', metaHeld);
};
function onKeyPress(button: string) {
if (VM === null) return;
let keysym = OSK_buttonToKeysym(button);
if (!keysym) {
console.error(`no keysym for ${button}, report this!`);
return;
}
switch (true) {
case button.startsWith('{shift'):
shiftHeld = !shiftHeld;
VM.key(keysym, shiftHeld);
break;
case button.startsWith('{control'):
ctrlHeld = !ctrlHeld;
VM.key(keysym, ctrlHeld);
break;
case button === '{capslock}':
capsHeld = !capsHeld;
VM.key(keysym, capsHeld);
break;
case button.startsWith('{alt'):
altHeld = !altHeld;
VM.key(keysym, altHeld);
break;
case button.startsWith('{meta'):
metaHeld = !metaHeld;
VM.key(keysym, metaHeld);
break;
default:
VM.key(keysym, true);
VM.key(keysym, false);
}
keyboard.setOptions({
layoutName: shiftHeld && capsHeld ? 'shiftcaps' : shiftHeld ? 'shift' : capsHeld ? 'capslock' : 'default'
});
updateOSKStyle();
}
/* End OSK */
let expectedClose = false;
let turn = -1;
// Listed VMs
const vms: VM[] = [];
const cards: HTMLDivElement[] = [];
const users: {
user: User;
usernameElement: HTMLSpanElement;
flagElement: HTMLSpanElement;
element: HTMLTableRowElement;
}[] = [];
let turnInterval: number | undefined = undefined;
let voteInterval: number | undefined = undefined;
let turnTimer = 0;
let voteTimer = 0;
let rank: Rank = Rank.Unregistered;
let perms: Permissions = new Permissions(0);
const chatsound = new Audio(Config.ChatSound);
// Active VM
let VM: CollabVMClient | null = null;
async function multicollab(url: string) {
// Create the client
let client = new CollabVMClient(url);
await client.WaitForOpen();
// Get the list of VMs
let list = await client.list();
// Get the number of online users
let online = client.getUsers().length;
// Close the client
client.close();
// Add to the list
vms.push(...list);
// Add to the DOM
for (let vm of list) {
let div = document.createElement('div');
div.classList.add('col-sm-5', 'col-md-3');
let card = document.createElement('div');
card.classList.add('card');
if (Config.NSFWVMs.indexOf(vm.id) !== -1) card.classList.add('cvm-nsfw');
card.setAttribute('data-cvm-node', vm.id);
card.addEventListener('click', async () => {
try {
await openVM(vm);
} catch (e) {
alert((e as Error).message);
}
});
vm.thumbnail.classList.add('card-img-top');
let cardBody = document.createElement('div');
cardBody.classList.add('card-body');
let cardTitle = document.createElement('h5');
cardTitle.innerHTML = Config.RawMessages.VMTitles ? vm.displayName : dompurify.sanitize(vm.displayName);
let usersOnline = document.createElement('span');
usersOnline.innerHTML = `(<i class="fa-solid fa-users"></i> ${online})`;
cardBody.appendChild(cardTitle);
cardBody.appendChild(usersOnline);
card.appendChild(vm.thumbnail);
card.appendChild(cardBody);
div.appendChild(card);
cards.push(div);
sortVMList();
}
}
async function openVM(vm: VM): Promise<void> {
// If there's an active VM it must be closed before opening another
if (VM !== null) return;
expectedClose = false;
// 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('flag', () => flag());
VM!.on('remuser', (user) => remUser(user));
VM!.on('rename', (oldname, newname, selfrename) => userRenamed(oldname, newname, selfrename));
VM!.on('renamestatus', (status) => {
// TODO: i18n these
switch (status) {
case 'taken':
alert(TheI18n.GetString(I18nStringKey.kError_UsernameTaken));
break;
case 'invalid':
alert(TheI18n.GetString(I18nStringKey.kError_UsernameInvalid));
break;
case 'blacklisted':
alert(TheI18n.GetString(I18nStringKey.kError_UsernameBlacklisted));
break;
}
});
VM!.on('turn', (status) => turnUpdate(status));
VM!.on('vote', (status: VoteStatus) => voteUpdate(status));
VM!.on('voteend', () => voteEnd());
VM!.on('votecd', (voteCooldown) => window.alert(TheI18n.GetString(I18nStringKey.kVM_VoteCooldownTimer, voteCooldown)));
VM!.on('login', (rank: Rank, perms: Permissions) => onLogin(rank, perms));
VM!.on('close', () => {
if (!expectedClose) alert(TheI18n.GetString(I18nStringKey.kError_UnexpectedDisconnection));
closeVM();
});
// auth
VM!.on('auth', async server => {
elements.changeUsernameBtn.style.display = "none";
if (Config.Auth.Enabled && Config.Auth.APIEndpoint === server && auth!.account) {
VM!.loginAccount(auth!.account.sessionToken);
} else if (!Config.Auth.Enabled || Config.Auth.APIEndpoint !== server) {
auth = new AuthManager(server);
await renderAuth();
}
});
// Wait for the client to open
await VM!.WaitForOpen();
// Connect to node
chatMessage('', `<b>${vm.id}</b><hr>`);
let username = Config.Auth.Enabled ? (auth!.account?.username ?? null) : localStorage.getItem('username');
let connected = await VM.connect(vm.id, username);
elements.adminInputVMID.value = vm.id;
w.VMName = vm.id;
if (!connected) {
// just give up
closeVM();
throw new Error('Failed to connect to node');
}
// Set the title
document.title = Format('{0} - {1}', vm.id, TheI18n.GetString(I18nStringKey.kGeneric_CollabVM));
// Append canvas
elements.vmDisplay.appendChild(VM!.canvas);
// Switch to the VM view
elements.vmlist.style.display = 'none';
elements.vmview.style.display = 'block';
return;
}
function closeVM() {
if (VM === null) return;
expectedClose = true;
// Close the VM
VM.close();
VM = null;
document.title = TheI18n.GetString(I18nStringKey.kGeneric_CollabVM);
turn = -1;
// Remove the canvas
elements.vmDisplay.innerHTML = '';
// Switch to the VM list
elements.vmlist.style.display = 'block';
elements.vmview.style.display = 'none';
// Clear users
users.splice(0, users.length);
elements.userlist.innerHTML = '';
rank = Rank.Unregistered;
perms.set(0);
w.VMName = null;
// Reset admin and vote panels
elements.staffbtns.style.display = 'none';
elements.restoreBtn.style.display = 'none';
elements.rebootBtn.style.display = 'none';
elements.bypassTurnBtn.style.display = 'none';
elements.endTurnBtn.style.display = 'none';
elements.clearQueueBtn.style.display = 'none';
elements.qemuMonitorBtn.style.display = 'none';
elements.indefTurnBtn.style.display = 'none';
elements.ghostTurnBtn.style.display = 'none';
elements.xssCheckboxContainer.style.display = 'none';
elements.forceVotePanel.style.display = 'none';
elements.voteResetPanel.style.display = 'none';
elements.voteYesLabel.innerText = '0';
elements.voteNoLabel.innerText = '0';
elements.xssCheckbox.checked = false;
elements.username.classList.remove('username-admin', 'username-moderator', 'username-registered');
elements.username.classList.add('username-unregistered');
// Reset rename button
elements.changeUsernameBtn.style.display = "inline-block";
// Reset auth if it was changed by the VM
if (Config.Auth.Enabled && auth?.apiEndpoint !== Config.Auth.APIEndpoint) {
auth = new AuthManager(Config.Auth.APIEndpoint);
renderAuth();
} else if (auth && !Config.Auth.Enabled) {
auth = null;
elements.accountDropdownMenuLink.style.display = "none";
}
}
async function loadList() {
var jsonVMs = Config.ServerAddressesListURI === null ? [] : await (await fetch(Config.ServerAddressesListURI)).json();
await Promise.all(
[Config.ServerAddresses, jsonVMs].flat().map((url) => {
return multicollab(url);
})
);
// automatically join the vm that's in the url if it exists in the node list
let v = vms.find((v) => v.id === window.location.hash.substring(1));
try {
if (v !== undefined) await openVM(v);
} catch (e) {
alert((e as Error).message);
}
}
function sortVMList() {
cards.sort((a, b) => {
return a.children[0].getAttribute('data-cvm-node')! > b.children[0].getAttribute('data-cvm-node')! ? 1 : -1;
});
elements.vmlist.children[0].innerHTML = '';
cards.forEach((c) => elements.vmlist.children[0].appendChild(c));
}
function sortUserList() {
users.sort((a, b) => {
if (a.user.username === w.username && a.user.turn >= b.user.turn && b.user.turn !== 0) return -1;
if (b.user.username === w.username && b.user.turn >= a.user.turn && a.user.turn !== 0) return 1;
if (a.user.turn === b.user.turn) return 0;
if (a.user.turn === -1) return 1;
if (b.user.turn === -1) return -1;
if (a.user.turn < b.user.turn) return -1;
else return 1;
});
for (const user of users) {
elements.userlist.removeChild(user.element);
elements.userlist.appendChild(user.element);
}
}
function chatMessage(username: string, message: string) {
let tr = document.createElement('tr');
let td = document.createElement('td');
if (!Config.RawMessages.Messages) message = dompurify.sanitize(message);
// System message
if (username === '') td.innerHTML = message;
else {
let user = VM!.getUsers().find((u) => u.username === username);
let rank;
if (user !== undefined) rank = user.rank;
else rank = Rank.Unregistered;
let userclass;
let msgclass;
switch (rank) {
case Rank.Unregistered:
userclass = 'chat-username-unregistered';
msgclass = 'chat-unregistered';
break;
case Rank.Registered:
userclass = 'chat-username-registered';
msgclass = 'chat-registered';
break;
case Rank.Admin:
userclass = 'chat-username-admin';
msgclass = 'chat-admin';
break;
case Rank.Moderator:
userclass = 'chat-username-moderator';
msgclass = 'chat-moderator';
break;
}
tr.classList.add(msgclass);
td.innerHTML = `<b class="${userclass}">${username}▸</b> ${message}`;
// hacky way to allow scripts
if (Config.RawMessages.Messages) 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;
chatsound.play();
}
function addUser(user: User) {
let olduser = users.find((u) => u.user === user);
if (olduser !== undefined) elements.userlist.removeChild(olduser.element);
let tr = document.createElement('tr');
tr.setAttribute('data-cvm-turn', '-1');
let td = document.createElement('td');
let flagSpan = document.createElement('span');
let usernameSpan = document.createElement('span');
flagSpan.classList.add("userlist-flag");
usernameSpan.classList.add("userlist-username");
td.appendChild(flagSpan);
if (user.countryCode !== null) flagSpan.innerHTML = getFlagEmoji(user.countryCode);
td.appendChild(usernameSpan);
usernameSpan.innerText = user.username;
switch (user.rank) {
case Rank.Admin:
tr.classList.add('user-admin');
break;
case Rank.Moderator:
tr.classList.add('user-moderator');
break;
case Rank.Registered:
tr.classList.add('user-registered');
break;
case Rank.Unregistered:
tr.classList.add('user-unregistered');
break;
}
if (user.username === w.username) tr.classList.add('user-current');
tr.appendChild(td);
let u = { user: user, element: tr, usernameElement: usernameSpan, flagElement: flagSpan };
if (rank === Rank.Admin || rank === Rank.Moderator) userModOptions(u);
elements.userlist.appendChild(tr);
if (olduser !== undefined) olduser.element = tr;
else users.push(u);
elements.onlineusercount.innerHTML = VM!.getUsers().length.toString();
}
function remUser(user: User) {
let olduser = users.findIndex((u) => u.user === user);
if (olduser !== undefined) elements.userlist.removeChild(users[olduser].element);
elements.onlineusercount.innerHTML = VM!.getUsers().length.toString();
users.splice(olduser, 1);
}
function getFlagEmoji(countryCode: string) {
if (countryCode.length !== 2) throw new Error('Invalid country code');
return String.fromCodePoint(...countryCode.toUpperCase().split('').map(char => 127397 + char.charCodeAt(0)));
}
function flag() {
for (let user of users.filter(u => u.user.countryCode !== null)) {
user.flagElement.innerHTML = getFlagEmoji(user.user.countryCode!);
}
}
function userRenamed(oldname: string, newname: string, selfrename: boolean) {
let user = users.find((u) => u.user.username === newname);
if (user) {
user.usernameElement.innerHTML = newname;
}
if (selfrename) {
w.username = newname;
elements.username.innerText = newname;
localStorage.setItem('username', newname);
}
}
function turnUpdate(status: TurnStatus) {
// Clear all turn data
turn = -1;
VM!.canvas.classList.remove('focused', 'waiting');
clearInterval(turnInterval);
turnTimer = 0;
for (const user of users) {
user.element.classList.remove('user-turn', 'user-waiting');
user.element.setAttribute('data-cvm-turn', '-1');
}
elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kVMButtons_TakeTurn);
enableOSK(false);
if (status.user !== null) {
let el = users.find((u) => u.user === status.user)!.element;
el!.classList.add('user-turn');
el!.setAttribute('data-cvm-turn', '0');
}
for (const user of status.queue) {
let el = users.find((u) => u.user === user)!.element;
el!.classList.add('user-waiting');
el.setAttribute('data-cvm-turn', status.queue.indexOf(user).toString(10));
}
if (status.user?.username === w.username) {
turn = 0;
turnTimer = status.turnTime! / 1000;
elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn);
VM!.canvas.classList.add('focused');
enableOSK(true);
}
if (status.queue.some((u) => u.username === w.username)) {
turn = status.queue.findIndex((u) => u.username === w.username) + 1;
turnTimer = status.queueTime! / 1000;
elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn);
VM!.canvas.classList.add('waiting');
}
if (turn === -1) elements.turnstatus.innerText = '';
else {
//@ts-ignore
turnInterval = setInterval(() => turnIntervalCb(), 1000);
setTurnStatus();
}
sortUserList();
}
function voteUpdate(status: VoteStatus) {
clearInterval(voteInterval);
elements.voteResetPanel.style.display = 'block';
elements.voteYesLabel.innerText = status.yesVotes.toString();
elements.voteNoLabel.innerText = status.noVotes.toString();
voteTimer = Math.floor(status.timeToEnd / 1000);
//@ts-ignore
voteInterval = setInterval(() => updateVoteEndTime(), 1000);
updateVoteEndTime();
}
function updateVoteEndTime() {
voteTimer--;
elements.voteTimeText.innerText = TheI18n.GetString(I18nStringKey.kVM_VoteForResetTimer, voteTimer);
if (voteTimer === 0) clearInterval(voteInterval);
}
function voteEnd() {
clearInterval(voteInterval);
elements.voteResetPanel.style.display = 'none';
}
function turnIntervalCb() {
turnTimer--;
setTurnStatus();
}
function setTurnStatus() {
if (turn === 0) elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_TurnTimeTimer, turnTimer);
else elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_WaitingTurnTimer, turnTimer);
}
function sendChat() {
if (VM === null) return;
if (elements.xssCheckbox.checked) VM.xss(elements.chatinput.value);
else VM.chat(elements.chatinput.value);
elements.chatinput.value = '';
}
// Bind list buttons
elements.homeBtn.addEventListener('click', () => closeVM());
// Bind VM view buttons
elements.sendChatBtn.addEventListener('click', sendChat);
elements.chatinput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendChat();
});
elements.changeUsernameBtn.addEventListener('click', () => {
let newname = prompt(TheI18n.GetString(I18nStringKey.kVMPrompts_EnterNewUsernamePrompt), w.username);
if (newname === w.username) return;
VM?.rename(newname);
});
elements.takeTurnBtn.addEventListener('click', () => {
VM?.turn(turn === -1);
});
elements.screenshotButton.addEventListener('click', () => {
if (!VM) return;
VM.canvas.toBlob((blob) => {
open(URL.createObjectURL(blob!), '_blank');
});
});
elements.ctrlAltDelBtn.addEventListener('click', () => {
if (!VM) return;
// Ctrl
VM?.key(0xffe3, true);
// Alt
VM?.key(0xffe9, true);
// Del
VM?.key(0xffff, true);
// Ctrl
VM?.key(0xffe3, false);
// Alt
VM?.key(0xffe9, false);
// Del
VM?.key(0xffff, false);
});
elements.voteResetButton.addEventListener('click', () => VM?.vote(true));
elements.voteYesBtn.addEventListener('click', () => VM?.vote(true));
elements.voteNoBtn.addEventListener('click', () => VM?.vote(false));
// Login
let usernameClick = false;
const loginModal = new bootstrap.Modal(elements.loginModal);
elements.loginModal.addEventListener('shown.bs.modal', () => elements.adminPassword.focus());
elements.username.addEventListener('click', () => {
if (auth) return;
if (!usernameClick) {
usernameClick = true;
setInterval(() => (usernameClick = false), 1000);
return;
}
loginModal.show();
});
elements.loginButton.addEventListener('click', () => doLogin());
elements.adminPassword.addEventListener('keypress', (e) => e.key === 'Enter' && doLogin());
elements.incorrectPasswordDismissBtn.addEventListener('click', () => (elements.badPasswordAlert.style.display = 'none'));
function doLogin() {
let adminPass = elements.adminPassword.value;
if (adminPass === '') return;
VM?.login(adminPass);
elements.adminPassword.value = '';
let u = VM?.on('login', () => {
u!();
loginModal.hide();
elements.badPasswordAlert.style.display = 'none';
});
let _u = VM?.on('badpw', () => {
_u!();
elements.badPasswordAlert.style.display = 'block';
});
}
function onLogin(_rank: Rank, _perms: Permissions) {
rank = _rank;
perms = _perms;
elements.username.classList.remove('username-unregistered', 'username-registered');
if (rank === Rank.Admin) elements.username.classList.add('username-admin');
if (rank === Rank.Moderator) elements.username.classList.add('username-moderator');
if (rank === Rank.Registered) elements.username.classList.add('username-registered');
elements.staffbtns.style.display = 'block';
if (_perms.restore) elements.restoreBtn.style.display = 'inline-block';
if (_perms.reboot) elements.rebootBtn.style.display = 'inline-block';
if (_perms.bypassturn) {
elements.bypassTurnBtn.style.display = 'inline-block';
elements.endTurnBtn.style.display = 'inline-block';
elements.clearQueueBtn.style.display = 'inline-block';
}
if (_rank === Rank.Admin) {
elements.qemuMonitorBtn.style.display = 'inline-block';
elements.indefTurnBtn.style.display = 'inline-block';
elements.ghostTurnBtn.style.display = 'inline-block';
}
if (_perms.xss) elements.xssCheckboxContainer.style.display = 'inline-block';
if (_perms.forcevote) elements.forceVotePanel.style.display = 'block';
if (rank !== Rank.Registered)
for (const user of users) userModOptions(user);
}
function userModOptions(user: { user: User; element: HTMLTableRowElement }) {
let tr = user.element;
let td = tr.children[0] as HTMLTableCellElement;
tr.classList.add('dropdown');
td.classList.add('dropdown-toggle');
td.setAttribute('data-bs-toggle', 'dropdown');
td.setAttribute('role', 'button');
td.setAttribute('aria-expanded', 'false');
let ul = document.createElement('ul');
ul.classList.add('dropdown-menu', 'dropdown-menu-dark', 'table-dark', 'text-light');
if (perms.bypassturn) addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn), () => VM!.endTurn(user.user.username), "mod-end-turn-btn");
if (perms.ban) addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_Ban), () => VM!.ban(user.user.username), "mod-ban-btn");
if (perms.kick) addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_Kick), () => VM!.kick(user.user.username), "mod-kick-btn");
if (perms.rename)
addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kVMButtons_ChangeUsername), () => {
let newname = prompt(TheI18n.GetString(I18nStringKey.kVMPrompts_AdminChangeUsernamePrompt, user.user.username));
if (!newname) return;
VM!.renameUser(user.user.username, newname);
}, "mod-rename-btn");
if (perms.mute) {
addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_TempMute), () => VM!.mute(user.user.username, MuteState.Temp), "mod-temp-mute-btn");
addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_IndefMute), () => VM!.mute(user.user.username, MuteState.Perma), "mod-indef-mute-btn");
addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_Unmute), () => VM!.mute(user.user.username, MuteState.Unmuted), "mod-unmute-btn");
}
if (perms.grabip)
addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_GetIP), async () => {
let ip = await VM!.getip(user.user.username);
alert(ip);
}, "mod-get-ip-btn");
tr.appendChild(ul);
}
function addUserDropdownItem(ul: HTMLUListElement, text: string, func: () => void, classname: string) {
let li = document.createElement('li');
let a = document.createElement('a');
a.href = '#';
a.classList.add('dropdown-item', classname);
a.innerHTML = text;
a.addEventListener('click', () => func());
li.appendChild(a);
ul.appendChild(li);
}
// Admin buttons
elements.restoreBtn.addEventListener('click', () => window.confirm(TheI18n.GetString(I18nStringKey.kVMPrompts_AdminRestoreVMPrompt)) && VM?.restore());
elements.rebootBtn.addEventListener('click', () => VM?.reboot());
elements.clearQueueBtn.addEventListener('click', () => VM?.clearQueue());
elements.bypassTurnBtn.addEventListener('click', () => VM?.bypassTurn());
elements.endTurnBtn.addEventListener('click', () => {
let user = VM?.getUsers().find((u) => u.turn === 0);
if (user) VM?.endTurn(user.username);
});
elements.forceVoteNoBtn.addEventListener('click', () => VM?.forceVote(false));
elements.forceVoteYesBtn.addEventListener('click', () => VM?.forceVote(true));
elements.indefTurnBtn.addEventListener('click', () => VM?.indefiniteTurn());
elements.ghostTurnBtn.addEventListener('click', () => {
w.collabvm.ghostTurn = !w.collabvm.ghostTurn;
if (w.collabvm.ghostTurn)
elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOn);
else
elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOff);
});
async function sendQEMUCommand() {
if (!elements.qemuMonitorInput.value) return;
let cmd = elements.qemuMonitorInput.value;
elements.qemuMonitorOutput.innerHTML += `&gt; ${cmd}\n`;
elements.qemuMonitorInput.value = '';
let response = await VM?.qemuMonitor(cmd);
elements.qemuMonitorOutput.innerHTML += `${response}\n`;
elements.qemuMonitorOutput.scrollTop = elements.qemuMonitorOutput.scrollHeight;
}
elements.qemuMonitorSendBtn.addEventListener('click', () => sendQEMUCommand());
elements.qemuMonitorInput.addEventListener('keypress', (e) => e.key === 'Enter' && sendQEMUCommand());
elements.osk.addEventListener('click', () => elements.oskContainer.classList.toggle('d-none'));
// Auth stuff
async function renderAuth() {
if (auth === null) throw new Error("Cannot renderAuth when auth is null.");
await auth.getAPIInformation();
elements.accountDropdownUsername.innerText = TheI18n.GetString(I18nStringKey.kNotLoggedIn);
elements.accountDropdownMenuLink.style.display = "block";
if (!auth!.info!.registrationOpen)
elements.accountRegisterButton.style.display = "none";
else
elements.accountRegisterButton.style.display = "block";
elements.accountLoginButton.style.display = "block";
elements.accountSettingsButton.style.display = "none";
elements.accountLogoutButton.style.display = "none";
elements.accountRegisterCaptcha.innerHTML = "";
elements.accountLoginCaptcha.innerHTML = "";
elements.accountResetPasswordCaptcha.innerHTML = "";
if (auth!.info!.hcaptcha.required) {
var hconfig = {sitekey: auth!.info!.hcaptcha.siteKey!};
hcaptcha.render(elements.accountRegisterCaptcha, hconfig);
hcaptcha.render(elements.accountLoginCaptcha, hconfig);
hcaptcha.render(elements.accountResetPasswordCaptcha, hconfig);
}
var token = localStorage.getItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
if (token) {
var result = await auth!.loadSession(token);
if (result.success) {
loadAccount();
} else {
localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
}
}
}
function loadAccount() {
if (auth === null || auth.account === null) throw new Error("Cannot loadAccount when auth or auth.account is null.");
elements.accountDropdownUsername.innerText = auth!.account!.username;
elements.accountLoginButton.style.display = "none";
elements.accountRegisterButton.style.display = "none";
elements.accountSettingsButton.style.display = "block";
elements.accountLogoutButton.style.display = "block";
if (VM) VM.loginAccount(auth.account.sessionToken);
}
const accountModal = new bootstrap.Modal(elements.accountModal);
elements.accountModalErrorDismiss.addEventListener('click', () => elements.accountModalError.style.display = "none");
elements.accountModalSuccessDismiss.addEventListener('click', () => elements.accountModalSuccess.style.display = "none");
elements.accountLoginButton.addEventListener("click", () => {
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kGeneric_Login);
elements.accountRegisterSection.style.display = "none";
elements.accountVerifyEmailSection.style.display = "none";
elements.accountLoginSection.style.display = "block";
elements.accountSettingsSection.style.display = "none";
elements.accountResetPasswordSection.style.display = "none";
elements.accountResetPasswordVerifySection.style.display = "none";
accountModal.show();
});
elements.accountRegisterButton.addEventListener("click", () => {
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kGeneric_Register);
elements.accountRegisterSection.style.display = "block";
elements.accountVerifyEmailSection.style.display = "none";
elements.accountLoginSection.style.display = "none";
elements.accountSettingsSection.style.display = "none";
elements.accountResetPasswordSection.style.display = "none";
elements.accountResetPasswordVerifySection.style.display = "none";
accountModal.show();
});
elements.accountSettingsButton.addEventListener("click", () => {
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_AccountSettings);
elements.accountRegisterSection.style.display = "none";
elements.accountVerifyEmailSection.style.display = "none";
elements.accountLoginSection.style.display = "none";
elements.accountSettingsSection.style.display = "block";
elements.accountResetPasswordSection.style.display = "none";
elements.accountResetPasswordVerifySection.style.display = "none";
// Fill fields
elements.accountSettingsUsername.value = auth!.account!.username;
elements.accountSettingsEmail.value = auth!.account!.email;
accountModal.show();
});
elements.accountLogoutButton.addEventListener('click', async () => {
if (!auth?.account) return;
await auth.logout();
localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
if (VM) closeVM();
renderAuth();
});
elements.accountForgotPasswordButton.addEventListener('click', () => {
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_ResetPassword);
elements.accountLoginSection.style.display = "none";
elements.accountResetPasswordSection.style.display = "block";
});
// i dont know if theres a better place to put this
let accountBeingVerified;
elements.accountLoginForm.addEventListener('submit', async (e) => {
e.preventDefault();
var hcaptchaToken = undefined;
var hcaptchaID = undefined;
if (auth!.info!.hcaptcha.required) {
hcaptchaID = elements.accountLoginCaptcha.children[0].getAttribute("data-hcaptcha-widget-id")!
var response = hcaptcha.getResponse(hcaptchaID);
if (response === "") {
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha);
elements.accountModalError.style.display = "block";
return false;
}
hcaptchaToken = response;
}
var username = elements.accountLoginUsername.value;
var password = elements.accountLoginPassword.value;
var result = await auth!.login(username, password, hcaptchaToken);
if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID);
if (result.success) {
elements.accountLoginUsername.value = "";
elements.accountLoginPassword.value = "";
if (result.verificationRequired) {
accountBeingVerified = result.username;
elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, result.email!);
elements.accountLoginSection.style.display = "none";
elements.accountVerifyEmailSection.style.display = "block";
return false;
}
localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.token!);
loadAccount();
accountModal.hide();
} else {
elements.accountModalErrorText.innerHTML = result.error!;
elements.accountModalError.style.display = "block";
}
return false;
});
elements.accountRegisterForm.addEventListener('submit', async (e) => {
e.preventDefault();
var hcaptchaToken = undefined;
var hcaptchaID = undefined;
if (auth!.info!.hcaptcha.required) {
hcaptchaID = elements.accountRegisterCaptcha.children[0].getAttribute("data-hcaptcha-widget-id")!
var response = hcaptcha.getResponse(hcaptchaID);
if (response === "") {
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha);
elements.accountModalError.style.display = "block";
return false;
}
hcaptchaToken = response;
}
var username = elements.accountRegisterUsername.value;
var password = elements.accountRegisterPassword.value;
var email = elements.accountRegisterEmail.value;
var dob = dayjs(elements.accountRegisterDateOfBirth.valueAsDate);
if (password !== elements.accountRegisterConfirmPassword.value) {
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch);
elements.accountModalError.style.display = "block";
return false;
}
var result = await auth!.register(username, password, email, dob, hcaptchaToken);
if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID);
if (result.success) {
elements.accountRegisterUsername.value = "";
elements.accountRegisterEmail.value = "";
elements.accountRegisterPassword.value = "";
elements.accountRegisterConfirmPassword.value = "";
elements.accountRegisterDateOfBirth.value = "";
if (result.verificationRequired) {
accountBeingVerified = result.username;
elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, result.email!);
elements.accountRegisterSection.style.display = "none";
elements.accountVerifyEmailSection.style.display = "block";
return false;
}
localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.sessionToken!);
await auth!.loadSession(result.sessionToken!);
loadAccount();
accountModal.hide();
} else {
elements.accountModalErrorText.innerHTML = result.error!;
elements.accountModalError.style.display = "block";
}
return false;
});
elements.accountVerifyEmailForm.addEventListener('submit', async e => {
e.preventDefault();
var username = accountBeingVerified!;
var code = elements.accountVerifyEmailCode.value;
var password = elements.accountVerifyEmailPassword.value;
var result = await auth!.verifyEmail(username, password, code);
if (result.success) {
elements.accountVerifyEmailCode.value = "";
elements.accountVerifyEmailPassword.value = "";
localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.sessionToken!);
await auth!.loadSession(result.sessionToken!);
loadAccount();
accountModal.hide();
} else {
elements.accountModalErrorText.innerHTML = result.error!;
elements.accountModalError.style.display = "block";
}
return false;
});
elements.accountSettingsForm.addEventListener('submit', async e => {
e.preventDefault();
var oldUsername = auth!.account!.username;
var oldEmail = auth!.account!.email;
var username = elements.accountSettingsUsername.value === auth!.account!.username ? undefined : elements.accountSettingsUsername.value;
var email = elements.accountSettingsEmail.value === auth!.account!.email ? undefined : elements.accountSettingsEmail.value;
var password = elements.accountSettingsNewPassword.value === "" ? undefined : elements.accountSettingsNewPassword.value;
var currentPassword = elements.accountSettingsCurrentPassword.value;
if (password && password !== elements.accountSettingsConfirmNewPassword.value) {
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch);
elements.accountModalError.style.display = "block";
return false;
}
localStorage.setItem("collabvm-hide-flag", JSON.stringify(elements.hideFlagCheckbox.checked));
if (!password && !email && !username) {
accountModal.hide();
return false
}
var result = await auth!.updateAccount(currentPassword, email, username, password);
if (result.success) {
elements.accountSettingsNewPassword.value = "";
elements.accountSettingsConfirmNewPassword.value = "";
elements.accountSettingsCurrentPassword.value = "";
if (result.verificationRequired) {
renderAuth();
accountBeingVerified = username ?? oldUsername;
elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, email ?? oldEmail);
elements.accountSettingsSection.style.display = "none";
elements.accountVerifyEmailSection.style.display = "block";
return false;
} else if (result.sessionExpired) {
accountModal.hide();
localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
if (VM) closeVM();
renderAuth();
} else {
accountModal.hide();
}
} else {
elements.accountModalErrorText.innerHTML = result.error!;
elements.accountModalError.style.display = "block";
}
return false;
});
let resetPasswordUsername;
let resetPasswordEmail;
elements.accountResetPasswordForm.addEventListener('submit', async e => {
e.preventDefault();
var hcaptchaToken = undefined;
var hcaptchaID = undefined;
if (auth!.info!.hcaptcha.required) {
hcaptchaID = elements.accountResetPasswordCaptcha.children[0].getAttribute("data-hcaptcha-widget-id")!
var response = hcaptcha.getResponse(hcaptchaID);
if (response === "") {
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha);
elements.accountModalError.style.display = "block";
return false;
}
hcaptchaToken = response;
}
var username = elements.accountResetPasswordUsername.value;
var email = elements.accountResetPasswordEmail.value;
var result = await auth!.sendPasswordResetEmail(username, email, hcaptchaToken);
if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID);
if (result.success) {
resetPasswordUsername = username;
resetPasswordEmail = email;
elements.accountResetPasswordUsername.value = "";
elements.accountResetPasswordEmail.value = "";
elements.accountVerifyPasswordResetText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyPasswordResetText, email);
elements.accountResetPasswordSection.style.display = "none";
elements.accountResetPasswordVerifySection.style.display = "block";
} else {
elements.accountModalErrorText.innerHTML = result.error!;
elements.accountModalError.style.display = "block";
}
return false;
});
elements.accountResetPasswordVerifyForm.addEventListener('submit', async e => {
e.preventDefault();
var code = elements.accountResetPasswordCode.value;
var password = elements.accountResetPasswordNewPassword.value;
if (password !== elements.accountResetPasswordConfirmNewPassword.value) {
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch);
elements.accountModalError.style.display = "block";
return false;
}
var result = await auth!.resetPassword(resetPasswordUsername!, resetPasswordEmail!, code, password);
if (result.success) {
elements.accountResetPasswordCode.value = "";
elements.accountResetPasswordNewPassword.value = "";
elements.accountResetPasswordConfirmNewPassword.value = "";
elements.accountModalSuccessText.innerHTML = TheI18n.GetString(I18nStringKey.kAccountModal_PasswordResetSuccess);
elements.accountModalSuccess.style.display = "block";
elements.accountResetPasswordVerifySection.style.display = "none";
elements.accountLoginSection.style.display = "block";
} else {
elements.accountModalErrorText.innerHTML = result.error!;
elements.accountModalError.style.display = "block";
}
return false;
});
let darkTheme = true;
function loadColorTheme(dark : boolean) {
if (dark) {
darkTheme = true;
document.children[0].setAttribute("data-bs-theme", "dark");
elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_LightMode);
elements.toggleThemeIcon.classList.remove("fa-moon");
elements.toggleThemeIcon.classList.add("fa-sun");
} else {
darkTheme = false;
document.children[0].setAttribute("data-bs-theme", "light");
elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_DarkMode);
elements.toggleThemeIcon.classList.remove("fa-sun");
elements.toggleThemeIcon.classList.add("fa-moon");
}
}
elements.toggleThemeBtn.addEventListener('click', e => {
e.preventDefault();
loadColorTheme(!darkTheme);
localStorage.setItem("cvm-dark-theme", darkTheme ? "1" : "0");
return false;
});
// Public API
w.collabvm = {
openVM: openVM,
closeVM: closeVM,
loadList: loadList,
multicollab: multicollab,
getVM: () => VM,
ghostTurn: false,
};
// Multicollab will stay in the global scope for backwards compatibility
w.multicollab = multicollab;
// Same goes for GetAdmin
w.GetAdmin = () => {
if (VM === null) return;
return {
adminInstruction: (...args: string[]) => {
args.unshift('admin');
VM?.send(...args);
},
restore: () => VM!.restore(),
reboot: () => VM!.reboot(),
clearQueue: () => VM!.clearQueue(),
bypassTurn: () => VM!.bypassTurn(),
endTurn: (username: string) => VM!.endTurn(username),
ban: (username: string) => VM!.ban(username),
kick: (username: string) => VM!.kick(username),
renameUser: (oldname: string, newname: string) => VM!.renameUser(oldname, newname),
mute: (username: string, state: number) => VM!.mute(username, state),
getip: (username: string) => VM!.getip(username),
qemuMonitor: (cmd: string) => {
VM?.qemuMonitor(cmd);
return;
},
globalXss: (msg: string) => VM!.xss(msg),
forceVote: (result: boolean) => VM!.forceVote(result)
};
};
// more backwards compatibility
w.cvmEvents = {
on: (event: string | number, cb: (...args: any) => void) => {
if (VM === null) return;
VM.on('message', (...args: any) => cb(...args));
}
};
w.VMName = null;
document.addEventListener('DOMContentLoaded', async () => {
// Initalize the i18n system
await TheI18n.Init();
TheI18n.on('languageChanged', lang => {
// Update all dynamic text
if (VM) {
document.title = Format('{0} - {1}', VM.getNode()!, TheI18n.GetString(I18nStringKey.kGeneric_CollabVM));
if (turn !== -1) {
if (turn === 0) elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_TurnTimeTimer, turnTimer);
else elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_WaitingTurnTimer, turnTimer);
elements.turnBtnText.innerText = TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn);
}
else
elements.turnBtnText.innerText = TheI18n.GetString(I18nStringKey.kVMButtons_TakeTurn);
if (VM!.getVoteStatus())
elements.voteTimeText.innerText = TheI18n.GetString(I18nStringKey.kVM_VoteForResetTimer, voteTimer);
}
else {
document.title = TheI18n.GetString(I18nStringKey.kGeneric_CollabVM);
}
if (!auth || !auth.account) elements.accountDropdownUsername.innerText = TheI18n.GetString(I18nStringKey.kNotLoggedIn);
if (darkTheme) elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_LightMode);
else elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_DarkMode);
if (w.collabvm.ghostTurn)
elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOn);
else
elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOff);
});
// Load theme
var _darktheme : boolean;
// Check if dark theme is set in local storage
if (localStorage.getItem("cvm-dark-theme") !== null)
loadColorTheme(localStorage.getItem("cvm-dark-theme") === "1");
// Otherwise, try to detect the system theme
else if (window.matchMedia('(prefers-color-scheme: dark)').matches)
loadColorTheme(true);
else
loadColorTheme(false);
// Initialize authentication if enabled
if (Config.Auth.Enabled) {
auth = new AuthManager(Config.Auth.APIEndpoint);
renderAuth();
}
var hideFlag = JSON.parse(localStorage.getItem("collabvm-hide-flag")!);
if (hideFlag === null) hideFlag = false;
elements.hideFlagCheckbox.checked = hideFlag;
document.title = TheI18n.GetString(I18nStringKey.kGeneric_CollabVM);
// Load all VMs
loadList();
// Welcome modal
let welcomeModal = new bootstrap.Modal(document.getElementById('welcomeModal') as HTMLDivElement);
let noWelcomeModal = window.localStorage.getItem(Config.WelcomeModalLocalStorageKey);
if (noWelcomeModal !== '1') {
let welcomeModalDismissBtn = document.getElementById('welcomeModalDismiss') as HTMLButtonElement;
welcomeModalDismissBtn.addEventListener('click', () => {
window.localStorage.setItem(Config.WelcomeModalLocalStorageKey, '1');
});
welcomeModalDismissBtn.disabled = true;
welcomeModal.show();
setTimeout(() => {
welcomeModalDismissBtn.disabled = false;
}, 5000);
}
elements.rulesBtn.addEventListener('click', e => {
if (TheI18n.CurrentLanguage() !== "en-us") {
e.preventDefault();
welcomeModal.show();
}
});
});