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 { 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) { 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 = `( ${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 { // 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('', `${vm.id}
`); 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 = `${username}▸ ${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); flagSpan.title = TheI18n.getCountryName(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!); user.flagElement.title = TheI18n.getCountryName(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 += `> ${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); for (const user of users) { if (user.user.countryCode !== null) { user.flagElement.title = TheI18n.getCountryName(user.user.countryCode); } } }); // 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(); } }); });