-
-
-
-
diff --git a/src/ts/AuthManager.ts b/src/ts/AuthManager.ts
deleted file mode 100644
index d23422f..0000000
--- a/src/ts/AuthManager.ts
+++ /dev/null
@@ -1,256 +0,0 @@
-import * as dayjs from 'dayjs';
-
-export default class AuthManager {
- apiEndpoint : string;
- info : AuthServerInformation | null;
- account : Account | null;
- constructor(apiEndpoint : string) {
- this.apiEndpoint = apiEndpoint;
- this.info = null;
- this.account = null;
- }
-
- getAPIInformation() : Promise {
- return new Promise(async res => {
- var data = await fetch(this.apiEndpoint + "/api/v1/info");
- this.info = await data.json();
- res(this.info!);
- })
- }
-
- login(username : string, password : string, captchaToken : string | undefined) : Promise {
- return new Promise(async (res,rej) => {
- if (!this.info) throw new Error("Cannot login before fetching API information.");
- if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
- var data = await fetch(this.apiEndpoint + "/api/v1/login", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- username: username,
- password: password,
- captchaToken: captchaToken
- })
- });
- var json = await data.json() as AccountLoginResult;
- if (!json) throw new Error("data.json() gave null or undefined result");
- if (json.success && !json.verificationRequired) {
- this.account = {
- username: json.username!,
- email: json.email!,
- sessionToken: json.token!
- }
- }
- res(json);
- })
- }
-
- loadSession(token : string) {
- return new Promise(async (res, rej) => {
- var data = await fetch(this.apiEndpoint + "/api/v1/session", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- token: token,
- })
- });
- var json = await data.json() as SessionResult;
- if (json.success) {
- this.account = {
- sessionToken: token,
- username: json.username!,
- email: json.email!,
- };
- }
- res(json);
- })
- }
-
- register(username : string, password : string, email : string, dateOfBirth : dayjs.Dayjs, captchaToken : string | undefined) : Promise {
- return new Promise(async (res, rej) => {
- if (!this.info) throw new Error("Cannot login before fetching API information.");
- if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
- var data = await fetch(this.apiEndpoint + "/api/v1/register", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- username: username,
- password: password,
- email: email,
- dateOfBirth: dateOfBirth.format("YYYY-MM-DD"),
- captchatoken: captchaToken
- })
- });
- res(await data.json() as AccountRegisterResult);
- });
- }
-
- logout() {
- return new Promise(async res => {
- if (!this.account) throw new Error("Cannot log out without logging in first");
- var data = await fetch(this.apiEndpoint + "/api/v1/logout", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- token: this.account.sessionToken
- })
- });
- var json = await data.json() as LogoutResult;
- this.account = null;
- res(json);
- })
- }
-
- verifyEmail(username : string, password : string, code : string) {
- return new Promise(async res => {
- var data = await fetch(this.apiEndpoint + "/api/v1/verify", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- username: username,
- password: password,
- code: code,
- })
- });
- res(await data.json() as VerifyEmailResult);
- });
- }
-
- updateAccount(currentPassword : string, newEmail : string | undefined, newUsername : string | undefined, newPassword : string | undefined) {
- return new Promise(async res => {
- if (!this.account) throw new Error("Cannot update account without being logged in.");
- if (!newEmail && !newUsername && !newPassword) throw new Error("Cannot update account without any new information.");
- var data = await fetch(this.apiEndpoint + "/api/v1/update", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- token: this.account!.sessionToken,
- currentPassword: currentPassword,
- newPassword: newPassword,
- username: newUsername,
- email: newEmail,
- })
- });
- var json = await data.json() as UpdateAccountResult;
- if (json.success) {
- if (this.account!.email !== newEmail) this.account!.email = newEmail!;
- if (this.account!.username !== newUsername) this.account!.username = newUsername!;
- if (json.sessionExpired || json.verificationRequired) {
- this.account = null;
- }
- }
- res(json);
- });
- }
-
- sendPasswordResetEmail(username : string, email : string, captchaToken : string | undefined) {
- return new Promise(async res => {
- if (!this.info) throw new Error("Cannot send password reset email without fetching API information.");
- if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
- var data = await fetch(this.apiEndpoint + "/api/v1/sendreset", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- username: username,
- email: email,
- captchaToken: captchaToken
- })
- });
- res(await data.json() as PasswordResetResult);
- });
- }
-
- resetPassword(username : string, email : string, code : string, newPassword : string) {
- return new Promise(async res => {
- var data = await fetch(this.apiEndpoint + "/api/v1/reset", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- username: username,
- email: email,
- code: code,
- newPassword: newPassword
- })
- });
- res(await data.json() as PasswordResetResult);
- });
- }
-}
-
-export interface AuthServerInformation {
- registrationOpen : boolean;
- hcaptcha : {
- required : boolean;
- siteKey : string | undefined;
- };
-}
-
-export interface AccountRegisterResult {
- success : boolean;
- error : string | undefined;
- verificationRequired : boolean | undefined;
- username : string | undefined;
- email : string | undefined;
- sessionToken : string | undefined;
-}
-
-export interface AccountLoginResult {
- success : boolean;
- token : string | undefined;
- error : string | undefined;
- verificationRequired : boolean | undefined;
- email : string | undefined;
- username : string | undefined;
-}
-
-export interface SessionResult {
- success : boolean;
- error : string | undefined;
- banned : boolean;
- username : string | undefined;
- email : string | undefined;
-}
-
-export interface VerifyEmailResult {
- success : boolean;
- error : string | undefined;
- sessionToken : string | undefined;
-}
-
-export interface LogoutResult {
- success : boolean;
- error : string | undefined;
-}
-
-export interface Account {
- username : string;
- email : string;
- sessionToken : string;
-}
-
-export interface UpdateAccountResult {
- success : boolean;
- error : string | undefined;
- verificationRequired : boolean | undefined;
- sessionExpired : boolean | undefined;
-}
-
-export interface PasswordResetResult {
- success : boolean;
- error : string | undefined;
-}
\ No newline at end of file
diff --git a/src/ts/StringLike.ts b/src/ts/StringLike.ts
deleted file mode 100644
index ecc6989..0000000
--- a/src/ts/StringLike.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// TODO: `Object` has a toString(), but we should probably gate that off
-/// Interface for things that can be turned into strings
-export interface ToStringable {
- toString(): string;
-}
-
-/// A type for strings, or things that can (in a valid manner) be turned into strings
-export type StringLike = string | ToStringable;
diff --git a/src/ts/fallbackLanguage.ts b/src/ts/fallbackLanguage.ts
deleted file mode 100644
index 01dcff3..0000000
--- a/src/ts/fallbackLanguage.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { Language } from "./i18n.js";
-
-const fallbackLanguage : Language = {
- "languageName": "English (US)",
- "translatedLanguageName": "English (US)",
- "flag": "🇺🇸",
- "author": "Computernewb",
-
- "stringKeys": {
- "kGeneric_CollabVM": "CollabVM",
- "kGeneric_Yes": "Yes",
- "kGeneric_No": "No",
- "kGeneric_Ok": "OK",
- "kGeneric_Cancel": "Cancel",
- "kGeneric_Send": "Send",
- "kGeneric_Understood": "Understood",
- "kGeneric_Username": "Username",
- "kGeneric_Password": "Password",
- "kGeneric_Login": "Log in",
- "kGeneric_Register": "Register",
- "kGeneric_EMail": "E-Mail",
- "kGeneric_DateOfBirth": "Date of Birth",
- "kGeneric_VerificationCode": "Verification Code",
- "kGeneric_Verify": "Verify",
- "kGeneric_Update": "Update",
- "kGeneric_Logout": "Log out",
-
- "kWelcomeModal_Header": "Welcome to CollabVM",
- "kWelcomeModal_Body": "
Before continuing, please familiarize yourself with our rules:
R1. Don't break the law.
Do not use CollabVM or CollabVM's network to violate United States federal law, New York state law, or international law. If CollabVM becomes aware a crime has been committed through its service, you will be immediately banned, and your activities may be reported to the authorities if necessary.
CollabVM is required by law to notify law enforcement agencies if it becomes aware of the presence of child pornography on, or being transmitted through its network.
COPPA is also enforced, please do not use CollabVM if you are under the age of 13 years old.
R2. No running DoS/DDoS tools.
Do not use CollabVM to DoS/DDoS an indivdiual, business, company, or anyone else.
R3. No spam distribution.
Do not spam any emails using this service or push spam in general.
R4. Do not abuse any exploits.
Do not abuse any exploits, additionally if you see someone abusing exploits or you need to report one, please contact me at: computernewbab@gmail.com
R5. Don't impersonate other users.
Do not impersonate other members of CollabVM. If caught, you'll be temporarily disconnected, and banned if necessary.
R6. One vote per person.
Do not use any methods or tools to bypass the vote restriction. Only one vote per person is allowed, no matter what. Anybody who is caught doing this will be banned.
R7. No Remote Administration Tools.
Do not use any remote administration tools (ex: DarkComet, NanoCore, Anydesk, TeamViewer, Orcus, etc.)
R8. No bypassing CollabNet.
Do not attempt to bypass the blocking provided by CollabNet, especially if it is being used to break Rule 1, Rule 2, or Rule 7 (or run stupid over-used things).
R9. No performing destructive actions constantly.
Any user may not destroy the VM (rendering it unusable constantly), install/reinstall the operating system (except on VM7 or VM8), or run bots that do such. This includes bots that spam massive amounts of keyboard/mouse input (\"kitting\").
R10. No Cryptomining
Attempting to mine cryptocurrency on the VMs will result in a kick, and then a permanent ban if you keep attempting. Besides, it's not like you're gonna make any money off it.
NSFW Warning
Please note that NSFW content is allowed on our anarchy VM (VM0b0t), and is viewed regularly. In addition, while we give a good effort to keep NSFW off the main VMs, people will occasionally slip it through.",
-
- "kSiteButtons_Home": "Home",
- "kSiteButtons_FAQ": "FAQ",
- "kSiteButtons_Rules": "Rules",
- "kSiteButtons_DarkMode": "Dark Mode",
- "kSiteButtons_LightMode": "Light Mode",
-
- "kVM_UsersOnlineText": "Users Online:",
-
- "kVM_TurnTimeTimer": "Turn expires in {0} seconds.",
- "kVM_WaitingTurnTimer": "Waiting for turn in {0} seconds.",
- "kVM_VoteCooldownTimer": "Please wait {0} seconds before starting another vote.",
-
- "kVM_VoteForResetTitle": "Do you want to reset the VM?",
- "kVM_VoteForResetTimer": "Vote ends in {0} seconds",
-
- "kVMButtons_TakeTurn": "Take Turn",
- "kVMButtons_EndTurn": "End Turn",
- "kVMButtons_ChangeUsername": "Change Username",
- "kVMButtons_Keyboard": "Keyboard",
- "KVMButtons_CtrlAltDel": "Ctrl+Alt+Del",
-
- "kVMButtons_VoteForReset": "Vote For Reset",
- "kVMButtons_Screenshot": "Screenshot",
-
- "kQEMUMonitor": "QEMU Monitor",
- "kAdminVMButtons_PassVote": "Pass Vote",
- "kAdminVMButtons_CancelVote": "Cancel Vote",
-
- "kAdminVMButtons_Restore": "Restore",
- "kAdminVMButtons_Reboot": "Reboot",
- "kAdminVMButtons_ClearTurnQueue": "Clear Turn Queue",
- "kAdminVMButtons_BypassTurn": "Bypass Turn",
- "kAdminVMButtons_IndefiniteTurn": "Indefinite Turn",
-
- "kAdminVMButtons_Ban": "Ban",
- "kAdminVMButtons_Kick": "Kick",
- "kAdminVMButtons_TempMute": "Temporary Mute",
- "kAdminVMButtons_IndefMute": "Indefinite Mute",
- "kAdminVMButtons_Unmute": "Unmute",
- "kAdminVMButtons_GetIP": "Get IP",
-
- "kVMPrompts_AdminChangeUsernamePrompt": "Enter new username for {0}:",
- "kVMPrompts_AdminRestoreVMPrompt": "Are you sure you want to restore the VM?",
- "kVMPrompts_EnterNewUsernamePrompt": "Enter a new username, or leave the field blank to be assigned a guest username",
-
- "kError_UnexpectedDisconnection": "You have been disconnected from the server.",
- "kError_UsernameTaken": "That username is already taken",
- "kError_UsernameInvalid": "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters.",
- "kError_UsernameBlacklisted": "That username has been blacklisted.",
- "kError_IncorrectPassword": "Incorrect password.",
-
- "kAccountModal_Verify": "Verify E-Mail",
- "kAccountModal_AccountSettings": "Account Settings",
- "kAccountModal_ResetPassword": "Reset Password",
-
- "kAccountModal_NewPassword": "New Password",
- "kAccountModal_ConfirmNewPassword": "Confirm New Password",
- "kAccountModal_CurrentPassword": "Current Password",
- "kAccountModal_ConfirmPassword": "Confirm Password",
-
- "kMissingCaptcha": "Please fill out the captcha.",
- "kPasswordsMustMatch": "Passwords must match.",
- "kAccountModal_VerifyText": "We sent an E-Mail to {0}. To verify your account, please enter the 8-digit code from the E-Mail below.",
- "kAccountModal_VerifyPasswordResetText": "We sent an E-Mail to {0}. To reset your password, please enter the 8-digit code from the E-Mail below.",
- "kAccountModal_PasswordResetSuccess": "Your password has been changed successfully. You can now log in with your new password.",
-
- "kNotLoggedIn": "Not Logged in"
- }
-}
-
-export default fallbackLanguage;
\ No newline at end of file
diff --git a/src/ts/format.ts b/src/ts/format.ts
deleted file mode 100644
index ae95f9d..0000000
--- a/src/ts/format.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { StringLike } from './StringLike';
-
-function isalpha(char: number) {
- return RegExp(/^\p{L}/, 'u').test(String.fromCharCode(char));
-}
-
-/// A simple function for formatting strings in a more expressive manner.
-/// While JavaScript *does* have string interpolation, it's not a total replacement
-/// for just formatting strings, and a method like this is better for data independent formatting.
-///
-/// ## Example usage
-///
-/// ```typescript
-/// let hello = Format("Hello, {0}!", "World");
-/// ```
-export function Format(pattern: string, ...args: Array) {
- let argumentsAsStrings: Array = [...args].map((el) => {
- // This catches cases where the thing already is a string
- if (typeof el == 'string') return el as string;
- return el.toString();
- });
-
- let pat = pattern;
-
- // Handle pattern ("{0} {1} {2} {3} {4} {5}") syntax if found
- for (let i = 0; i < pat.length; ++i) {
- if (pat[i] == '{') {
- let replacementStart = i;
- let foundSpecifierEnd = false;
-
- // Make sure the specifier is not cut off (the last character of the string)
- if (i + 3 > pat.length) {
- throw new Error(`Error in format pattern "${pat}": Cutoff/invalid format specifier`);
- }
-
- // Try and find the specifier end ('}').
- // Whitespace and a '{' are considered errors.
- for (let j = i + 1; j < pat.length; ++j) {
- switch (pat[j]) {
- case '}':
- foundSpecifierEnd = true;
- i = j;
- break;
-
- case '{':
- throw new Error(`Error in format pattern "${pat}": Cannot start a format specifier in an existing replacement`);
- case ' ':
- throw new Error(`Error in format pattern "${pat}": Whitespace inside format specifier`);
-
- case '-':
- throw new Error(`Error in format pattern "${pat}": Malformed format specifier`);
-
- default:
- if (isalpha(pat.charCodeAt(j))) throw new Error(`Error in format pattern "${pat}": Malformed format specifier`);
- break;
- }
-
- if (foundSpecifierEnd) break;
- }
-
- if (!foundSpecifierEnd) throw new Error(`Error in format pattern "${pat}": No terminating "}" character found`);
-
- // Get the beginning and trailer
- let beginning = pat.substring(0, replacementStart);
- let trailer = pat.substring(replacementStart + 3);
-
- let argumentIndex = parseInt(pat.substring(replacementStart + 1, i));
- if (Number.isNaN(argumentIndex) || argumentIndex > argumentsAsStrings.length) throw new Error(`Error in format pattern "${pat}": Argument index out of bounds`);
-
- // This is seriously the only decent way to do this in javascript
- // thanks brendan eich (replace this thanking with more choice words in your head)
- pat = beginning + argumentsAsStrings[argumentIndex] + trailer;
- }
- }
-
- return pat;
-}
diff --git a/src/ts/i18n.ts b/src/ts/i18n.ts
deleted file mode 100644
index 48db09d..0000000
--- a/src/ts/i18n.ts
+++ /dev/null
@@ -1,442 +0,0 @@
-import { StringLike } from './StringLike';
-import { Format } from './format';
-import { Emitter, Unsubscribe, createNanoEvents } from 'nanoevents';
-import Config from '../../config.json';
-
-/// All string keys.
-export enum I18nStringKey {
- // Generic things
- kGeneric_CollabVM = 'kGeneric_CollabVM',
- kGeneric_Yes = 'kGeneric_Yes',
- kGeneric_No = 'kGeneric_No',
- kGeneric_Ok = 'kGeneric_Ok',
- kGeneric_Cancel = 'kGeneric_Cancel',
- kGeneric_Send = 'kGeneric_Send',
- kGeneric_Understood = 'kGeneric_Understood',
- kGeneric_Username = 'kGeneric_Username',
- kGeneric_Password = 'kGeneric_Password',
- kGeneric_Login = 'kGeneric_Login',
- kGeneric_Register = 'kGeneric_Register',
- kGeneric_EMail = 'kGeneric_EMail',
- kGeneric_DateOfBirth = 'kGeneric_DateOfBirth',
- kGeneric_VerificationCode = 'kGeneric_VerificationCode',
- kGeneric_Verify = 'kGeneric_Verify',
- kGeneric_Update = 'kGeneric_Update',
- kGeneric_Logout = 'kGeneric_Logout',
-
- kWelcomeModal_Header = 'kWelcomeModal_Header',
- kWelcomeModal_Body = 'kWelcomeModal_Body',
-
- kSiteButtons_Home = 'kSiteButtons_Home',
- kSiteButtons_FAQ = 'kSiteButtons_FAQ',
- kSiteButtons_Rules = 'kSiteButtons_Rules',
- kSiteButtons_DarkMode = 'kSiteButtons_DarkMode',
- kSiteButtons_LightMode = 'kSiteButtons_LightMode',
- kSiteButtons_Languages = 'kSiteButtons_Languages',
-
- kVM_UsersOnlineText = 'kVM_UsersOnlineText',
-
- kVM_TurnTimeTimer = 'kVM_TurnTimeTimer',
- kVM_WaitingTurnTimer = 'kVM_WaitingTurnTimer',
- kVM_VoteCooldownTimer = 'kVM_VoteCooldownTimer',
-
- kVM_VoteForResetTitle = 'kVM_VoteForResetTitle',
- kVM_VoteForResetTimer = 'kVM_VoteForResetTimer',
-
- kVMButtons_TakeTurn = 'kVMButtons_TakeTurn',
- kVMButtons_EndTurn = 'kVMButtons_EndTurn',
- kVMButtons_ChangeUsername = 'kVMButtons_ChangeUsername',
- kVMButtons_Keyboard = 'kVMButtons_Keyboard',
- KVMButtons_CtrlAltDel = 'KVMButtons_CtrlAltDel',
-
- kVMButtons_VoteForReset = 'kVMButtons_VoteForReset',
- kVMButtons_Screenshot = 'kVMButtons_Screenshot',
-
- // Admin VM buttons
- kQEMUMonitor = 'kQEMUMonitor',
- kAdminVMButtons_PassVote = 'kAdminVMButtons_PassVote',
- kAdminVMButtons_CancelVote = 'kAdminVMButtons_CancelVote',
-
- kAdminVMButtons_Restore = 'kAdminVMButtons_Restore',
- kAdminVMButtons_Reboot = 'kAdminVMButtons_Reboot',
- kAdminVMButtons_ClearTurnQueue = 'kAdminVMButtons_ClearTurnQueue',
- kAdminVMButtons_BypassTurn = 'kAdminVMButtons_BypassTurn',
- kAdminVMButtons_IndefiniteTurn = 'kAdminVMButtons_IndefiniteTurn',
-
- kAdminVMButtons_Ban = 'kAdminVMButtons_Ban',
- kAdminVMButtons_Kick = 'kAdminVMButtons_Kick',
- kAdminVMButtons_TempMute = 'kAdminVMButtons_TempMute',
- kAdminVMButtons_IndefMute = 'kAdminVMButtons_IndefMute',
- kAdminVMButtons_Unmute = 'kAdminVMButtons_Unmute',
- kAdminVMButtons_GetIP = 'kAdminVMButtons_GetIP',
-
- // prompts
- kVMPrompts_AdminChangeUsernamePrompt = 'kVMPrompts_AdminChangeUsernamePrompt',
- kVMPrompts_AdminRestoreVMPrompt = 'kVMPrompts_AdminRestoreVMPrompt',
- kVMPrompts_EnterNewUsernamePrompt = 'kVMPrompts_EnterNewUsernamePrompt',
-
- // error messages
- kError_UnexpectedDisconnection = 'kError_UnexpectedDisconnection',
-
- kError_UsernameTaken = 'kError_UsernameTaken',
- kError_UsernameInvalid = 'kError_UsernameInvalid',
- kError_UsernameBlacklisted = 'kError_UsernameBlacklisted',
- kError_IncorrectPassword = 'kError_IncorrectPassword',
-
- // Auth
- kAccountModal_Verify = 'kAccountModal_Verify',
- kAccountModal_AccountSettings = 'kAccountModal_AccountSettings',
- kAccountModal_ResetPassword = 'kAccountModal_ResetPassword',
-
- kAccountModal_NewPassword = 'kAccountModal_NewPassword',
- kAccountModal_ConfirmNewPassword = 'kAccountModal_ConfirmNewPassword',
- kAccountModal_CurrentPassword = 'kAccountModal_CurrentPassword',
- kAccountModal_ConfirmPassword = 'kAccountModal_ConfirmPassword',
-
- kAccountModal_VerifyText = 'kAccountModal_VerifyText',
- kAccountModal_VerifyPasswordResetText = 'kAccountModal_VerifyPasswordResetText',
- kAccountModal_PasswordResetSuccess = 'kAccountModal_PasswordResetSuccess',
- kMissingCaptcha = 'kMissingCaptcha',
- kPasswordsMustMatch = 'kPasswordsMustMatch',
-
- kNotLoggedIn = 'kNotLoggedIn',
-}
-
-export interface I18nEvents {
- // Called when the language is changed
- languageChanged: (lang: string) => void;
-}
-
-// This models the JSON structure.
-export type Language = {
- languageName: string;
- translatedLanguageName: string;
- flag: string; // country flag, can be blank if not applicable. will be displayed in language dropdown
- author: string;
-
- stringKeys: {
- // This is fancy typescript speak for
- // "any string index returns a string",
- // which is our expectation.
- // See https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures if this is confusing.
- [key: string]: string;
- };
-};
-
-// `languages.json`
-export type LanguagesJson = {
- // Array of language IDs to allow loading
- languages: Array;
-
- // The default language (set if a invalid language not in the languages array is set, or no language is set)
- defaultLanguage: string;
-};
-
-// ID for fallback language
-const fallbackId = '!!fallback';
-
-// This language is provided in the webapp itself just in case language stuff fails
-import fallbackLanguage from './fallbackLanguage.js';
-
-interface StringKeyMap {
- [k: string]: I18nStringKey;
-}
-
-/// our fancy internationalization helper.
-export class I18n {
- // The language data itself
- private langs : Map = new Map();
- private lang: Language = fallbackLanguage;
- private languageDropdown: HTMLSpanElement = document.getElementById('languageDropdown') as HTMLSpanElement;
- private emitter: Emitter = createNanoEvents();
-
- CurrentLanguage = () => this.langId;
-
- // the ID of the language
- private langId: string = fallbackId;
-
- async Init() {
- // Load language list
- var res = await fetch("lang/languages.json");
- if (!res.ok) {
- alert("Failed to load languages.json: " + res.statusText);
- this.SetLanguage(fallbackLanguage, fallbackId);
- this.ReplaceStaticStrings();
- return;
- }
- var langData = await res.json() as LanguagesJson;
- for (const langId of langData.languages) {
- let path = `./lang/${langId}.json`;
- let res = await fetch(path);
- if (!res.ok) {
- console.error(`Failed to load lang/${langId}.json: ${res.statusText}`);
- continue;
- }
- let _lang = await res.json() as Language;
- this.langs.set(langId, _lang);
- }
- this.langs.forEach((_lang, langId) => {
- // Add to language dropdown
- var a = document.createElement('a');
- a.classList.add('dropdown-item');
- a.href = '#';
- a.innerText = `${_lang.flag} ${_lang.languageName}`;
- a.addEventListener('click', (e) => {
- e.preventDefault();
- this.SetLanguage(_lang, langId);
- this.ReplaceStaticStrings();
- });
- this.languageDropdown.appendChild(a);
- });
- let lang = null;
- let lsLang = window.localStorage.getItem('i18n-lang');
- var browserLang = navigator.language.toLowerCase();
- // If the language is set in localstorage, use that
- if (lsLang !== null && this.langs.has(lsLang)) lang = lsLang;
- // If the browser language is in the list, use that
- else if (this.langs.has(browserLang)) lang = browserLang;
- else {
- // If the exact browser language isn't in the list, try to find a language with the same prefix
- for (let langId of langData.languages) {
- if (langId.split('-')[0] === browserLang.split('-')[0]) {
- lang = langId;
- break;
- }
- }
- }
- // If all else fails, use the default language
- if (lang === null) lang = langData.defaultLanguage;
- this.SetLanguage(this.langs.get(lang) as Language, lang);
- this.ReplaceStaticStrings();
- }
-
- private SetLanguage(lang: Language, id: string) {
- let lastId = this.langId;
- this.langId = id;
- this.lang = lang;
-
- // Only replace static strings
- if (this.langId != lastId) this.ReplaceStaticStrings();
-
- // Set the language ID localstorage entry
- if (this.langId !== fallbackId) {
- window.localStorage.setItem('i18n-lang', this.langId);
- }
-
- this.emitter.emit('languageChanged', this.langId);
- console.log('i18n initalized for', id, 'sucessfully!');
- }
-
- // Replaces static strings that we don't recompute
- private ReplaceStaticStrings() {
- const kDomIdtoStringMap: StringKeyMap = {
- siteNameText: I18nStringKey.kGeneric_CollabVM,
- homeBtnText: I18nStringKey.kSiteButtons_Home,
- faqBtnText: I18nStringKey.kSiteButtons_FAQ,
- rulesBtnText: I18nStringKey.kSiteButtons_Rules,
- accountLoginButton: I18nStringKey.kGeneric_Login,
- accountRegisterButton: I18nStringKey.kGeneric_Register,
- accountSettingsButton: I18nStringKey.kAccountModal_AccountSettings,
- accountLogoutButton: I18nStringKey.kGeneric_Logout,
- languageDropdownText: I18nStringKey.kSiteButtons_Languages,
-
- welcomeModalHeader: I18nStringKey.kWelcomeModal_Header,
- welcomeModalBody: I18nStringKey.kWelcomeModal_Body,
- welcomeModalDismiss: I18nStringKey.kGeneric_Understood,
-
- usersOnlineText: I18nStringKey.kVM_UsersOnlineText,
-
- voteResetHeaderText: I18nStringKey.kVM_VoteForResetTitle,
- voteYesBtnText: I18nStringKey.kGeneric_Yes,
- voteNoBtnText: I18nStringKey.kGeneric_No,
-
- changeUsernameBtnText: I18nStringKey.kVMButtons_ChangeUsername,
- oskBtnText: I18nStringKey.kVMButtons_Keyboard,
- ctrlAltDelBtnText: I18nStringKey.KVMButtons_CtrlAltDel,
- voteForResetBtnText: I18nStringKey.kVMButtons_VoteForReset,
- screenshotBtnText: I18nStringKey.kVMButtons_Screenshot,
-
- // admin stuff
- badPasswordAlertText: I18nStringKey.kError_IncorrectPassword,
- loginModalPasswordText: I18nStringKey.kGeneric_Password,
- loginButton: I18nStringKey.kGeneric_Login,
- passVoteBtnText: I18nStringKey.kAdminVMButtons_PassVote,
- cancelVoteBtnText: I18nStringKey.kAdminVMButtons_CancelVote,
- endTurnBtnText: I18nStringKey.kVMButtons_EndTurn,
- qemuMonitorBtnText: I18nStringKey.kQEMUMonitor,
- qemuModalHeader: I18nStringKey.kQEMUMonitor,
- qemuMonitorSendBtn: I18nStringKey.kGeneric_Send,
-
- restoreBtnText: I18nStringKey.kAdminVMButtons_Restore,
- rebootBtnText: I18nStringKey.kAdminVMButtons_Reboot,
- clearQueueBtnText: I18nStringKey.kAdminVMButtons_ClearTurnQueue,
- bypassTurnBtnText: I18nStringKey.kAdminVMButtons_BypassTurn,
- indefTurnBtnText: I18nStringKey.kAdminVMButtons_IndefiniteTurn,
-
- // Account modal
- accountLoginUsernameLabel: I18nStringKey.kGeneric_Username,
- accountLoginPasswordLabel: I18nStringKey.kGeneric_Password,
- accountModalLoginBtn: I18nStringKey.kGeneric_Login,
- accountForgotPasswordButton: I18nStringKey.kAccountModal_ResetPassword,
- accountRegisterEmailLabel: I18nStringKey.kGeneric_EMail,
- accountRegisterUsernameLabel: I18nStringKey.kGeneric_Username,
- accountRegisterPasswordLabel: I18nStringKey.kGeneric_Password,
- accountRegisterConfirmPasswordLabel: I18nStringKey.kAccountModal_ConfirmPassword,
- accountRegisterDateOfBirthLabel: I18nStringKey.kGeneric_DateOfBirth,
- accountModalRegisterBtn: I18nStringKey.kGeneric_Register,
- accountVerifyEmailCodeLabel: I18nStringKey.kGeneric_VerificationCode,
- accountVerifyEmailPasswordLabel: I18nStringKey.kGeneric_Password,
- accountModalVerifyEmailBtn: I18nStringKey.kGeneric_Verify,
- accountSettingsEmailLabel: I18nStringKey.kGeneric_EMail,
- accountSettingsUsernameLabel: I18nStringKey.kGeneric_Username,
- accountSettingsNewPasswordLabel: I18nStringKey.kAccountModal_NewPassword,
- accountSettingsConfirmNewPasswordLabel: I18nStringKey.kAccountModal_ConfirmNewPassword,
- accountSettingsCurrentPasswordLabel: I18nStringKey.kAccountModal_CurrentPassword,
- updateAccountSettingsBtn: I18nStringKey.kGeneric_Update,
- accountResetPasswordEmailLabel: I18nStringKey.kGeneric_EMail,
- accountResetPasswordUsernameLabel: I18nStringKey.kGeneric_Username,
- accountResetPasswordBtn: I18nStringKey.kAccountModal_ResetPassword,
- accountResetPasswordCodeLabel: I18nStringKey.kGeneric_VerificationCode,
- accountResetPasswordNewPasswordLabel: I18nStringKey.kAccountModal_NewPassword,
- accountResetPasswordConfirmNewPasswordLabel: I18nStringKey.kAccountModal_ConfirmNewPassword,
- accountResetPasswordVerifyBtn: I18nStringKey.kAccountModal_ResetPassword,
- };
-
- const kDomAttributeToStringMap = {
- adminPassword: {
- placeholder: I18nStringKey.kGeneric_Password,
- },
- accountLoginUsername: {
- placeholder: I18nStringKey.kGeneric_Username,
- },
- accountLoginPassword: {
- placeholder: I18nStringKey.kGeneric_Password,
- },
- accountRegisterEmail: {
- placeholder: I18nStringKey.kGeneric_EMail,
- },
- accountRegisterUsername: {
- placeholder: I18nStringKey.kGeneric_Username,
- },
- accountRegisterPassword: {
- placeholder: I18nStringKey.kGeneric_Password,
- },
- accountRegisterConfirmPassword: {
- placeholder: I18nStringKey.kAccountModal_ConfirmPassword,
- },
- accountVerifyEmailCode: {
- placeholder: I18nStringKey.kGeneric_VerificationCode,
- },
- accountVerifyEmailPassword: {
- placeholder: I18nStringKey.kGeneric_Password,
- },
- accountSettingsEmail: {
- placeholder: I18nStringKey.kGeneric_EMail,
- },
- accountSettingsUsername: {
- placeholder: I18nStringKey.kGeneric_Username,
- },
- accountSettingsNewPassword: {
- placeholder: I18nStringKey.kAccountModal_NewPassword,
- },
- accountSettingsConfirmNewPassword: {
- placeholder: I18nStringKey.kAccountModal_ConfirmNewPassword,
- },
- accountSettingsCurrentPassword: {
- placeholder: I18nStringKey.kAccountModal_CurrentPassword,
- },
- accountResetPasswordEmail: {
- placeholder: I18nStringKey.kGeneric_EMail,
- },
- accountResetPasswordUsername: {
- placeholder: I18nStringKey.kGeneric_Username,
- },
- accountResetPasswordCode: {
- placeholder: I18nStringKey.kGeneric_VerificationCode,
- },
- accountResetPasswordNewPassword: {
- placeholder: I18nStringKey.kAccountModal_NewPassword,
- },
- accountResetPasswordConfirmNewPassword: {
- placeholder: I18nStringKey.kAccountModal_ConfirmNewPassword,
- },
- };
-
- const kDomClassToStringMap: StringKeyMap = {
- "mod-end-turn-btn": I18nStringKey.kVMButtons_EndTurn,
- "mod-ban-btn": I18nStringKey.kAdminVMButtons_Ban,
- "mod-kick-btn": I18nStringKey.kAdminVMButtons_Kick,
- "mod-change-username-btn": I18nStringKey.kVMButtons_ChangeUsername,
- "mod-temp-mute-btn": I18nStringKey.kAdminVMButtons_TempMute,
- "mod-indef-mute-btn": I18nStringKey.kAdminVMButtons_IndefMute,
- "mod-unmute-btn": I18nStringKey.kAdminVMButtons_Unmute,
- "mod-get-ip-btn": I18nStringKey.kAdminVMButtons_GetIP,
- }
-
- for (let domId of Object.keys(kDomIdtoStringMap)) {
- let element = document.getElementById(domId);
- if (element == null) {
- alert(`Error: Could not find element with ID ${domId} in the DOM! Please tell a site admin this happened.`);
- return;
- }
-
- // Do the magic.
- // N.B: For now, we assume all strings in this map are not formatted.
- // If this assumption changes, then we should just use GetString() again
- // and maybe include arguments, but for now this is okay
- element.innerHTML = this.GetStringRaw(kDomIdtoStringMap[domId]);
- }
-
- for (let domId of Object.keys(kDomAttributeToStringMap)) {
- let element = document.getElementById(domId);
- if (element == null) {
- alert(`Error: Could not find element with ID ${domId} in the DOM! Please tell a site admin this happened.`);
- return;
- }
-
- // TODO: Figure out if we can get rid of this ts-ignore
- // @ts-ignore
- let attributes = kDomAttributeToStringMap[domId];
- for (let attr of Object.keys(attributes)) {
- element.setAttribute(attr, this.GetStringRaw(attributes[attr] as I18nStringKey));
- }
- }
-
- for (let domClass of Object.keys(kDomClassToStringMap)) {
- let elements = document.getElementsByClassName(domClass);
- for (let element of elements) {
- element.innerHTML = this.GetStringRaw(kDomClassToStringMap[domClass]);
- }
- }
- }
-
- // Returns a (raw, unformatted) string. Currently only used if we don't need formatting.
- GetStringRaw(key: I18nStringKey): string {
- if (key === I18nStringKey.kGeneric_CollabVM && Config.SiteNameOverride) return Config.SiteNameOverride;
- if (key === I18nStringKey.kWelcomeModal_Header && Config.WelcomeModalTitleOverride) return Config.WelcomeModalTitleOverride;
- if (key === I18nStringKey.kWelcomeModal_Body && Config.WelcomeModalBodyOverride) return Config.WelcomeModalBodyOverride;
- let val = this.lang.stringKeys[key];
-
- // Look up the fallback language by default if the language doesn't
- // have that string key yet; if the fallback doesn't have it either,
- // then just return the string key and a bit of a notice things have gone wrong
- if (val == undefined) {
- let fallback = fallbackLanguage.stringKeys[key];
- if (fallback !== undefined) val = fallback;
- else return `${key} (ERROR LOOKING UP TRANSLATION!!!)`;
- }
-
- return val;
- }
-
- // Returns a formatted localized string.
- GetString(key: I18nStringKey, ...replacements: StringLike[]): string {
- return Format(this.GetStringRaw(key), ...replacements);
- }
-
- on(event: e, cb: I18nEvents[e]): Unsubscribe {
- return this.emitter.on(event, cb);
- }
-}
-
-export let TheI18n = new I18n();
\ No newline at end of file
diff --git a/src/ts/keyboard.ts b/src/ts/keyboard.ts
deleted file mode 100644
index 9e558c2..0000000
--- a/src/ts/keyboard.ts
+++ /dev/null
@@ -1,408 +0,0 @@
-// Pulled a bunch of functions out of the guac source code to get a keysym
-// and then a wrapper
-// shitty but it works so /shrug
-// THIS SUCKS SO BAD AND I HATE IT PLEASE REWRITE ALL OF THIS
-
-export default function GetKeysym(keyCode: number, key: string, location: number): number | null {
- let keysym = keysym_from_key_identifier(key, location) || keysym_from_keycode(keyCode, location);
- return keysym;
-}
-
-function keysym_from_key_identifier(identifier: string, location: number): number | null {
- if (!identifier) return null;
-
- let typedCharacter: string | undefined;
-
- // If identifier is U+xxxx, decode Unicode character
- const unicodePrefixLocation = identifier.indexOf('U+');
- if (unicodePrefixLocation >= 0) {
- const hex = identifier.substring(unicodePrefixLocation + 2);
- typedCharacter = String.fromCharCode(parseInt(hex, 16));
- } else if (identifier.length === 1) typedCharacter = identifier;
- else return get_keysym(keyidentifier_keysym[identifier], location);
-
- if (!typedCharacter) return null;
-
- const codepoint = typedCharacter.charCodeAt(0);
- return keysym_from_charcode(codepoint);
-}
-
-function get_keysym(keysyms: number[] | null, location: number): number | null {
- if (!keysyms) return null;
- return keysyms[location] || keysyms[0];
-}
-
-function keysym_from_charcode(codepoint: number): number | null {
- if (isControlCharacter(codepoint)) return 0xff00 | codepoint;
- if (codepoint >= 0x0000 && codepoint <= 0x00ff) return codepoint;
- if (codepoint >= 0x0100 && codepoint <= 0x10ffff) return 0x01000000 | codepoint;
- return null;
-}
-
-function isControlCharacter(codepoint: number): boolean {
- return codepoint <= 0x1f || (codepoint >= 0x7f && codepoint <= 0x9f);
-}
-
-function keysym_from_keycode(keyCode: number, location: number): number | null {
- return get_keysym(keycodeKeysyms[keyCode], location);
-}
-
-function key_identifier_sane(keyCode: number, keyIdentifier: string): boolean {
- if (!keyIdentifier) return false;
- const unicodePrefixLocation = keyIdentifier.indexOf('U+');
- if (unicodePrefixLocation === -1) return true;
-
- const codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation + 2), 16);
- if (keyCode !== codepoint) return true;
- if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57)) return true;
- return false;
-}
-
-export function OSK_buttonToKeysym(button: string): number | null {
- const keyMapping = OSK_keyMappings.find((mapping) => mapping.includes(button));
- if (keyMapping) {
- const [, keyCode, keyIdentifier, key, location] = keyMapping;
- return GetKeysym(keyCode, key, location);
- }
- return null;
-}
-
-interface KeyIdentifierKeysym {
- [key: string]: number[] | null;
-}
-
-interface KeyCodeKeysyms {
- [key: number]: number[] | null;
-}
-
-const keycodeKeysyms: KeyCodeKeysyms = {
- 8: [0xff08], // backspace
- 9: [0xff09], // tab
- 12: [0xff0b, 0xff0b, 0xff0b, 0xffb5], // clear / KP 5
- 13: [0xff0d], // enter
- 16: [0xffe1, 0xffe1, 0xffe2], // shift
- 17: [0xffe3, 0xffe3, 0xffe4], // ctrl
- 18: [0xffe9, 0xffe9, 0xfe03], // alt
- 19: [0xff13], // pause/break
- 20: [0xffe5], // caps lock
- 27: [0xff1b], // escape
- 32: [0x0020], // space
- 33: [0xff55, 0xff55, 0xff55, 0xffb9], // page up / KP 9
- 34: [0xff56, 0xff56, 0xff56, 0xffb3], // page down / KP 3
- 35: [0xff57, 0xff57, 0xff57, 0xffb1], // end / KP 1
- 36: [0xff50, 0xff50, 0xff50, 0xffb7], // home / KP 7
- 37: [0xff51, 0xff51, 0xff51, 0xffb4], // left arrow / KP 4
- 38: [0xff52, 0xff52, 0xff52, 0xffb8], // up arrow / KP 8
- 39: [0xff53, 0xff53, 0xff53, 0xffb6], // right arrow / KP 6
- 40: [0xff54, 0xff54, 0xff54, 0xffb2], // down arrow / KP 2
- 45: [0xff63, 0xff63, 0xff63, 0xffb0], // insert / KP 0
- 46: [0xffff, 0xffff, 0xffff, 0xffae], // delete / KP decimal
- 91: [0xffeb], // left window key (hyper_l)
- 92: [0xff67], // right window key (menu key?)
- 93: null, // select key
- 96: [0xffb0], // KP 0
- 97: [0xffb1], // KP 1
- 98: [0xffb2], // KP 2
- 99: [0xffb3], // KP 3
- 100: [0xffb4], // KP 4
- 101: [0xffb5], // KP 5
- 102: [0xffb6], // KP 6
- 103: [0xffb7], // KP 7
- 104: [0xffb8], // KP 8
- 105: [0xffb9], // KP 9
- 106: [0xffaa], // KP multiply
- 107: [0xffab], // KP add
- 109: [0xffad], // KP subtract
- 110: [0xffae], // KP decimal
- 111: [0xffaf], // KP divide
- 112: [0xffbe], // f1
- 113: [0xffbf], // f2
- 114: [0xffc0], // f3
- 115: [0xffc1], // f4
- 116: [0xffc2], // f5
- 117: [0xffc3], // f6
- 118: [0xffc4], // f7
- 119: [0xffc5], // f8
- 120: [0xffc6], // f9
- 121: [0xffc7], // f10
- 122: [0xffc8], // f11
- 123: [0xffc9], // f12
- 144: [0xff7f], // num lock
- 145: [0xff14], // scroll lock
- 225: [0xfe03] // altgraph (iso_level3_shift)
-};
-
-const keyidentifier_keysym: KeyIdentifierKeysym = {
- Again: [0xff66],
- AllCandidates: [0xff3d],
- Alphanumeric: [0xff30],
- Alt: [0xffe9, 0xffe9, 0xfe03],
- Attn: [0xfd0e],
- AltGraph: [0xfe03],
- ArrowDown: [0xff54],
- ArrowLeft: [0xff51],
- ArrowRight: [0xff53],
- ArrowUp: [0xff52],
- Backspace: [0xff08],
- CapsLock: [0xffe5],
- Cancel: [0xff69],
- Clear: [0xff0b],
- Convert: [0xff21],
- Copy: [0xfd15],
- Crsel: [0xfd1c],
- CrSel: [0xfd1c],
- CodeInput: [0xff37],
- Compose: [0xff20],
- Control: [0xffe3, 0xffe3, 0xffe4],
- ContextMenu: [0xff67],
- DeadGrave: [0xfe50],
- DeadAcute: [0xfe51],
- DeadCircumflex: [0xfe52],
- DeadTilde: [0xfe53],
- DeadMacron: [0xfe54],
- DeadBreve: [0xfe55],
- DeadAboveDot: [0xfe56],
- DeadUmlaut: [0xfe57],
- DeadAboveRing: [0xfe58],
- DeadDoubleacute: [0xfe59],
- DeadCaron: [0xfe5a],
- DeadCedilla: [0xfe5b],
- DeadOgonek: [0xfe5c],
- DeadIota: [0xfe5d],
- DeadVoicedSound: [0xfe5e],
- DeadSemivoicedSound: [0xfe5f],
- Delete: [0xffff],
- Down: [0xff54],
- End: [0xff57],
- Enter: [0xff0d],
- EraseEof: [0xfd06],
- Escape: [0xff1b],
- Execute: [0xff62],
- Exsel: [0xfd1d],
- ExSel: [0xfd1d],
- F1: [0xffbe],
- F2: [0xffbf],
- F3: [0xffc0],
- F4: [0xffc1],
- F5: [0xffc2],
- F6: [0xffc3],
- F7: [0xffc4],
- F8: [0xffc5],
- F9: [0xffc6],
- F10: [0xffc7],
- F11: [0xffc8],
- F12: [0xffc9],
- F13: [0xffca],
- F14: [0xffcb],
- F15: [0xffcc],
- F16: [0xffcd],
- F17: [0xffce],
- F18: [0xffcf],
- F19: [0xffd0],
- F20: [0xffd1],
- F21: [0xffd2],
- F22: [0xffd3],
- F23: [0xffd4],
- F24: [0xffd5],
- Find: [0xff68],
- GroupFirst: [0xfe0c],
- GroupLast: [0xfe0e],
- GroupNext: [0xfe08],
- GroupPrevious: [0xfe0a],
- FullWidth: null,
- HalfWidth: null,
- HangulMode: [0xff31],
- Hankaku: [0xff29],
- HanjaMode: [0xff34],
- Help: [0xff6a],
- Hiragana: [0xff25],
- HiraganaKatakana: [0xff27],
- Home: [0xff50],
- Hyper: [0xffed, 0xffed, 0xffee],
- Insert: [0xff63],
- JapaneseHiragana: [0xff25],
- JapaneseKatakana: [0xff26],
- JapaneseRomaji: [0xff24],
- JunjaMode: [0xff38],
- KanaMode: [0xff2d],
- KanjiMode: [0xff21],
- Katakana: [0xff26],
- Left: [0xff51],
- Meta: [0xffe7, 0xffe7, 0xffe8],
- ModeChange: [0xff7e],
- NumLock: [0xff7f],
- PageDown: [0xff56],
- PageUp: [0xff55],
- Pause: [0xff13],
- Play: [0xfd16],
- PreviousCandidate: [0xff3e],
- PrintScreen: [0xfd1d],
- Redo: [0xff66],
- Right: [0xff53],
- RomanCharacters: null,
- Scroll: [0xff14],
- Select: [0xff60],
- Separator: [0xffac],
- Shift: [0xffe1, 0xffe1, 0xffe2],
- SingleCandidate: [0xff3c],
- Super: [0xffeb, 0xffeb, 0xffec],
- Tab: [0xff09],
- Up: [0xff52],
- Undo: [0xff65],
- Win: [0xffeb],
- Zenkaku: [0xff28],
- ZenkakuHankaku: [0xff2a]
-};
-
-const OSK_keyMappings: [string, number, string, string, number][] = [
- ['!', 49, 'Digit1', '!', 0],
- ['#', 51, 'Digit3', '#', 0],
- ['$', 52, 'Digit4', '$', 0],
- ['%', 53, 'Digit5', '%', 0],
- ['&', 55, 'Digit7', '&', 0],
- ["'", 222, 'Quote', "'", 0],
- ['(', 57, 'Digit9', '(', 0],
- [')', 48, 'Digit0', ')', 0],
- ['*', 56, 'Digit8', '*', 0],
- ['+', 187, 'Equal', '+', 0],
- [',', 188, 'Comma', ',', 0],
- ['-', 189, 'Minus', '-', 0],
- ['.', 190, 'Period', '.', 0],
- ['/', 191, 'Slash', '/', 0],
- ['0', 48, 'Digit0', '0', 0],
- ['1', 49, 'Digit1', '1', 0],
- ['2', 50, 'Digit2', '2', 0],
- ['3', 51, 'Digit3', '3', 0],
- ['4', 52, 'Digit4', '4', 0],
- ['5', 53, 'Digit5', '5', 0],
- ['6', 54, 'Digit6', '6', 0],
- ['7', 55, 'Digit7', '7', 0],
- ['8', 56, 'Digit8', '8', 0],
- ['9', 57, 'Digit9', '9', 0],
- [':', 186, 'Semicolon', ':', 0],
- [';', 186, 'Semicolon', ';', 0],
- ['<', 188, 'Comma', '<', 0],
- ['=', 187, 'Equal', '=', 0],
- ['>', 190, 'Period', '>', 0],
- ['?', 191, 'Slash', '?', 0],
- ['@', 50, 'Digit2', '@', 0],
- ['A', 65, 'KeyA', 'A', 0],
- ['B', 66, 'KeyB', 'B', 0],
- ['C', 67, 'KeyC', 'C', 0],
- ['D', 68, 'KeyD', 'D', 0],
- ['E', 69, 'KeyE', 'E', 0],
- ['F', 70, 'KeyF', 'F', 0],
- ['G', 71, 'KeyG', 'G', 0],
- ['H', 72, 'KeyH', 'H', 0],
- ['I', 73, 'KeyI', 'I', 0],
- ['J', 74, 'KeyJ', 'J', 0],
- ['K', 75, 'KeyK', 'K', 0],
- ['L', 76, 'KeyL', 'L', 0],
- ['M', 77, 'KeyM', 'M', 0],
- ['N', 78, 'KeyN', 'N', 0],
- ['O', 79, 'KeyO', 'O', 0],
- ['P', 80, 'KeyP', 'P', 0],
- ['Q', 81, 'KeyQ', 'Q', 0],
- ['R', 82, 'KeyR', 'R', 0],
- ['S', 83, 'KeyS', 'S', 0],
- ['T', 84, 'KeyT', 'T', 0],
- ['U', 85, 'KeyU', 'U', 0],
- ['V', 86, 'KeyV', 'V', 0],
- ['W', 87, 'KeyW', 'W', 0],
- ['X', 88, 'KeyX', 'X', 0],
- ['Y', 89, 'KeyY', 'Y', 0],
- ['Z', 90, 'KeyZ', 'Z', 0],
- ['[', 219, 'BracketLeft', '[', 0],
- ['\\', 220, 'Backslash', '\\', 0],
- [']', 221, 'BracketRight', ']', 0],
- ['^', 54, 'Digit6', '^', 0],
- ['_', 189, 'Minus', '_', 0],
- ['`', 192, 'Backquote', '`', 0],
- ['a', 65, 'KeyA', 'a', 0],
- ['b', 66, 'KeyB', 'b', 0],
- ['c', 67, 'KeyC', 'c', 0],
- ['d', 68, 'KeyD', 'd', 0],
- ['e', 69, 'KeyE', 'e', 0],
- ['f', 70, 'KeyF', 'f', 0],
- ['g', 71, 'KeyG', 'g', 0],
- ['h', 72, 'KeyH', 'h', 0],
- ['i', 73, 'KeyI', 'i', 0],
- ['j', 74, 'KeyJ', 'j', 0],
- ['k', 75, 'KeyK', 'k', 0],
- ['l', 76, 'KeyL', 'l', 0],
- ['m', 77, 'KeyM', 'm', 0],
- ['n', 78, 'KeyN', 'n', 0],
- ['o', 79, 'KeyO', 'o', 0],
- ['p', 80, 'KeyP', 'p', 0],
- ['q', 81, 'KeyQ', 'q', 0],
- ['r', 82, 'KeyR', 'r', 0],
- ['s', 83, 'KeyS', 's', 0],
- ['t', 84, 'KeyT', 't', 0],
- ['u', 85, 'KeyU', 'u', 0],
- ['v', 86, 'KeyV', 'v', 0],
- ['w', 87, 'KeyW', 'w', 0],
- ['x', 88, 'KeyX', 'x', 0],
- ['y', 89, 'KeyY', 'y', 0],
- ['z', 90, 'KeyZ', 'z', 0],
- ['{', 219, 'BracketLeft', '{', 0],
- ['{altleft}', 18, 'AltLeft', 'AltLeft', 1],
- ['{altright}', 18, 'AltRight', 'AltRight', 2],
- ['{arrowdown}', 40, 'ArrowDown', 'ArrowDown', 0],
- ['{arrowleft}', 37, 'ArrowLeft', 'ArrowLeft', 0],
- ['{arrowright}', 39, 'ArrowRight', 'ArrowRight', 0],
- ['{arrowup}', 38, 'ArrowUp', 'ArrowUp', 0],
- ['{backspace}', 8, 'Backspace', 'Backspace', 0],
- ['{capslock}', 20, 'CapsLock', 'CapsLock', 0],
- ['{controlleft}', 17, 'ControlLeft', 'ControlLeft', 1],
- ['{controlright}', 17, 'ControlRight', 'ControlRight', 2],
- ['{delete}', 46, 'Delete', 'Delete', 0],
- ['{end}', 35, 'End', 'End', 0],
- ['{enter}', 13, 'Enter', 'Enter', 0],
- ['{escape}', 27, 'Escape', 'Escape', 0],
- ['{f10}', 121, 'F10', 'F10', 0],
- ['{f11}', 122, 'F11', 'F11', 0],
- ['{f12}', 123, 'F12', 'F12', 0],
- ['{f1}', 112, 'F1', 'F1', 0],
- ['{f2}', 113, 'F2', 'F2', 0],
- ['{f3}', 114, 'F3', 'F3', 0],
- ['{f4}', 115, 'F4', 'F4', 0],
- ['{f5}', 116, 'F5', 'F5', 0],
- ['{f6}', 117, 'F6', 'F6', 0],
- ['{f7}', 118, 'F7', 'F7', 0],
- ['{f8}', 119, 'F8', 'F8', 0],
- ['{f9}', 120, 'F9', 'F9', 0],
- ['{home}', 36, 'Home', 'Home', 0],
- ['{insert}', 45, 'Insert', 'Insert', 0],
- ['{metaleft}', 91, 'OSLeft', 'OSLeft', 1],
- ['{metaright}', 92, 'OSRight', 'OSRight', 2],
- ['{numlock}', 144, 'NumLock', 'NumLock', 0],
- ['{numpad0}', 96, 'Numpad0', 'Numpad0', 3],
- ['{numpad1}', 97, 'Numpad1', 'Numpad1', 3],
- ['{numpad2}', 98, 'Numpad2', 'Numpad2', 3],
- ['{numpad3}', 99, 'Numpad3', 'Numpad3', 3],
- ['{numpad4}', 100, 'Numpad4', 'Numpad4', 3],
- ['{numpad5}', 101, 'Numpad5', 'Numpad5', 3],
- ['{numpad6}', 102, 'Numpad6', 'Numpad6', 3],
- ['{numpad7}', 103, 'Numpad7', 'Numpad7', 3],
- ['{numpad8}', 104, 'Numpad8', 'Numpad8', 3],
- ['{numpad9}', 105, 'Numpad9', 'Numpad9', 3],
- ['{numpadadd}', 107, 'NumpadAdd', 'NumpadAdd', 3],
- ['{numpaddecimal}', 110, 'NumpadDecimal', 'NumpadDecimal', 3],
- ['{numpaddivide}', 111, 'NumpadDivide', 'NumpadDivide', 3],
- ['{numpadenter}', 13, 'NumpadEnter', 'NumpadEnter', 3],
- ['{numpadmultiply}', 106, 'NumpadMultiply', 'NumpadMultiply', 3],
- ['{numpadsubtract}', 109, 'NumpadSubtract', 'NumpadSubtract', 3],
- ['{pagedown}', 34, 'PageDown', 'PageDown', 0],
- ['{pageup}', 33, 'PageUp', 'PageUp', 0],
- ['{pause}', 19, 'Pause', 'Pause', 0],
- ['{prtscr}', 44, 'PrintScreen', 'PrintScreen', 0],
- ['{scrolllock}', 145, 'ScrollLock', 'ScrollLock', 0],
- ['{shiftleft}', 16, 'ShiftLeft', 'ShiftLeft', 1],
- ['{shiftright}', 16, 'ShiftRight', 'ShiftRight', 2],
- ['{space}', 32, 'Space', 'Space', 0],
- ['{tab}', 9, 'Tab', 'Tab', 0],
- ['|', 220, 'Backslash', '|', 0],
- ['}', 221, 'BracketRight', '}', 0],
- ['~', 192, 'Backquote', '~', 0],
- ['"', 222, 'Quote', '"', 0]
-];
diff --git a/src/ts/main.ts b/src/ts/main.ts
deleted file mode 100644
index 1e4e566..0000000
--- a/src/ts/main.ts
+++ /dev/null
@@ -1,1321 +0,0 @@
-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,
- 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,
-
- 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;
- 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('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.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');
- td.innerHTML = 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 };
- 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 userRenamed(oldname: string, newname: string, selfrename: boolean) {
- let user = users.find((u) => u.user.username === newname);
- if (user) {
- user.element.children[0].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';
- }
- 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());
-
-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;
- }
- 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
-};
-// 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);
-
- });
- // 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();
- }
-
- 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();
- }
- });
-});
diff --git a/src/ts/protocol/CollabVMClient.ts b/src/ts/protocol/CollabVMClient.ts
deleted file mode 100644
index dbc5200..0000000
--- a/src/ts/protocol/CollabVMClient.ts
+++ /dev/null
@@ -1,685 +0,0 @@
-import { createNanoEvents, Emitter, DefaultEvents, Unsubscribe } from 'nanoevents';
-import * as Guacutils from './Guacutils.js';
-import VM from './VM.js';
-import { User } from './User.js';
-import { AdminOpcode, Permissions, Rank } from './Permissions.js';
-import TurnStatus from './TurnStatus.js';
-import Mouse from './mouse.js';
-import GetKeysym from '../keyboard.js';
-import VoteStatus from './VoteStatus.js';
-import MuteState from './MuteState.js';
-import { StringLike } from '../StringLike.js';
-
-export interface CollabVMClientEvents {
- //open: () => void;
- close: () => void;
-
- message: (...args: string[]) => void;
-
- // Protocol stuff
- chat: (username: string, message: string) => void;
-
- adduser: (user: User) => void;
- remuser: (user: User) => void;
-
- renamestatus: (status: 'taken' | 'invalid' | 'blacklisted') => void;
- turn: (status: TurnStatus) => void;
-
- rename: (oldUsername: string, newUsername: string, selfRename: boolean) => void;
-
- vote: (status: VoteStatus) => void;
- voteend: () => void;
- votecd: (coolDownTime: number) => void;
-
- badpw: () => void;
- login: (rank: Rank, perms: Permissions) => void;
-
- // Auth stuff
- auth: (server: string) => void;
- accountlogin: (success: boolean) => void;
-}
-
-// types for private emitter
-interface CollabVMClientPrivateEvents {
- open: () => void;
- list: (listEntries: string[]) => void;
- connect: (connectedToVM: boolean) => void;
- ip: (username: string, ip: string) => void;
- qemu: (qemuResponse: string) => void;
-}
-
-export default class CollabVMClient {
- // Fields
- private socket: WebSocket;
- canvas: HTMLCanvasElement;
- // A secondary canvas that is not scaled
- unscaledCanvas: HTMLCanvasElement;
- canvasScale : { width : number, height : number } = { width: 0, height: 0 };
- actualScreenSize : { width : number, height : number } = { width: 0, height: 0 };
- private unscaledCtx: CanvasRenderingContext2D;
- private ctx: CanvasRenderingContext2D;
- private url: string;
- private connectedToVM: boolean = false;
- private users: User[] = [];
- private username: string | null = null;
- private mouse: Mouse = new Mouse();
- private rank: Rank = Rank.Unregistered;
- private perms: Permissions = new Permissions(0);
- private voteStatus: VoteStatus | null = null;
- private node: string | null = null;
- private auth: boolean = false;
- // events that are used internally and not exposed
- private internalEmitter: Emitter;
- // public events
- private publicEmitter: Emitter;
-
- private unsubscribeCallbacks: Array = [];
-
- constructor(url: string) {
- // Save the URL
- this.url = url;
- // Create the events
- this.internalEmitter = createNanoEvents();
- this.publicEmitter = createNanoEvents();
- // Create the canvas
- this.canvas = document.createElement('canvas');
- this.unscaledCanvas = document.createElement('canvas');
- // Set tab index so it can be focused
- this.canvas.tabIndex = -1;
- // Get the 2D context
- this.ctx = this.canvas.getContext('2d')!;
- this.unscaledCtx = this.unscaledCanvas.getContext('2d')!;
- // Bind canvas click
- this.canvas.addEventListener('click', (e) => {
- if (this.users.find((u) => u.username === this.username)?.turn === -1) this.turn(true);
- });
-
- // Bind keyboard and mouse
- this.canvas.addEventListener(
- 'mousedown',
- (e: MouseEvent) => {
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
- this.mouse.initFromMouseEvent(e);
- this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
- },
- {
- capture: true
- }
- );
-
- this.canvas.addEventListener(
- 'mouseup',
- (e: MouseEvent) => {
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
- this.mouse.initFromMouseEvent(e);
- this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
- },
- {
- capture: true
- }
- );
-
- this.canvas.addEventListener(
- 'mousemove',
- (e: MouseEvent) => {
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
- this.mouse.initFromMouseEvent(e);
- this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
- },
- {
- capture: true
- }
- );
-
- this.canvas.addEventListener(
- 'keydown',
- (e: KeyboardEvent) => {
- e.preventDefault();
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
- let keysym = GetKeysym(e.keyCode, e.key, e.location);
- if (keysym === null) return;
- this.key(keysym, true);
- },
- {
- capture: true
- }
- );
-
- this.canvas.addEventListener(
- 'keyup',
- (e: KeyboardEvent) => {
- e.preventDefault();
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
- let keysym = GetKeysym(e.keyCode, e.key, e.location);
- if (keysym === null) return;
- this.key(keysym, false);
- },
- {
- capture: true
- }
- );
-
- this.canvas.addEventListener(
- 'wheel',
- (ev: WheelEvent) => {
- ev.preventDefault();
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
- this.mouse.initFromWheelEvent(ev);
-
- this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
-
- // this is a very, very ugly hack but it seems to work so /shrug
- if (this.mouse.scrollUp) this.mouse.scrollUp = false;
- else if (this.mouse.scrollDown) this.mouse.scrollDown = false;
-
- this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
- },
- {
- capture: true
- }
- );
- window.addEventListener('resize', (e) => this.onWindowResize(e));
- this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
- // Create the WebSocket
- this.socket = new WebSocket(url, 'guacamole');
- // Add the event listeners
- this.socket.addEventListener('open', () => this.onOpen());
- this.socket.addEventListener('message', (event) => this.onMessage(event));
- this.socket.addEventListener('close', () => this.publicEmitter.emit('close'));
- }
-
- // Fires when the WebSocket connection is opened
- private onOpen() {
- this.internalEmitter.emit('open');
- }
-
- // Fires on WebSocket message
- private onMessage(event: MessageEvent) {
- let msgArr: string[];
- try {
- msgArr = Guacutils.decode(event.data);
- } catch (e) {
- console.error(`Server sent invalid message (${e})`);
- return;
- }
- this.publicEmitter.emit('message', ...msgArr);
- switch (msgArr[0]) {
- case 'nop': {
- // Send a NOP back
- this.send('nop');
- break;
- }
- case 'list': {
- // pass msgarr to the emitter for processing by list()
- this.internalEmitter.emit('list', msgArr.slice(1));
- break;
- }
- case 'connect': {
- this.connectedToVM = msgArr[1] === '1';
- this.internalEmitter.emit('connect', this.connectedToVM);
- break;
- }
- case 'size': {
- if (msgArr[1] !== '0') return;
- this.recalculateCanvasScale(parseInt(msgArr[2]), parseInt(msgArr[3]));
- this.unscaledCanvas.width = this.actualScreenSize.width;
- this.unscaledCanvas.height = this.actualScreenSize.height;
- this.canvas.width = this.canvasScale.width;
- this.canvas.height = this.canvasScale.height;
- break;
- }
- case 'png': {
- // Despite the opcode name, this is actually JPEG, because old versions of the server used PNG and yknow backwards compatibility
- let img = new Image();
- var x = parseInt(msgArr[3]);
- var y = parseInt(msgArr[4]);
- img.addEventListener('load', () => {
- if (this.actualScreenSize.width !== this.canvasScale.width || this.actualScreenSize.height !== this.canvasScale.height)
- this.unscaledCtx.drawImage(img, x, y);
- // Scale the image to the canvas
- this.ctx.drawImage(img, 0, 0, img.width, img.height,
- (x / this.actualScreenSize.width) * this.canvas.width,
- (y / this.actualScreenSize.height) * this.canvas.height,
- (img.width / this.actualScreenSize.width) * this.canvas.width,
- (img.height / this.actualScreenSize.height) * this.canvas.height
- );
- });
- img.src = 'data:image/jpeg;base64,' + msgArr[5];
- break;
- }
- case 'chat': {
- for (let i = 1; i < msgArr.length; i += 2) {
- this.publicEmitter.emit('chat', msgArr[i], msgArr[i + 1]);
- }
- break;
- }
- case 'adduser': {
- for (let i = 2; i < msgArr.length; i += 2) {
- let _user = this.users.find((u) => u.username === msgArr[i]);
- if (_user !== undefined) {
- _user.rank = parseInt(msgArr[i + 1]);
- } else {
- _user = new User(msgArr[i], parseInt(msgArr[i + 1]));
- this.users.push(_user);
- }
- this.publicEmitter.emit('adduser', _user);
- }
- break;
- }
- case 'remuser': {
- for (let i = 2; i < msgArr.length; i++) {
- let _user = this.users.find((u) => u.username === msgArr[i]);
- if (_user === undefined) continue;
- this.users.splice(this.users.indexOf(_user), 1);
- this.publicEmitter.emit('remuser', _user);
- }
- }
- case 'rename': {
- let selfrename = false;
- let oldusername: string | null = null;
- // We've been renamed
- if (msgArr[1] === '0') {
- selfrename = true;
- oldusername = this.username;
- // msgArr[2] is the status of the rename
- // Anything other than 0 is an error, however the server will still rename us to a guest name
- switch (msgArr[2]) {
- case '1':
- // The username we wanted was taken
- this.publicEmitter.emit('renamestatus', 'taken');
- break;
- case '2':
- // The username we wanted was invalid
- this.publicEmitter.emit('renamestatus', 'invalid');
- break;
- case '3':
- // The username we wanted is blacklisted
- this.publicEmitter.emit('renamestatus', 'blacklisted');
- break;
- }
- this.username = msgArr[3];
- } else oldusername = msgArr[2];
- let _user = this.users.find((u) => u.username === oldusername);
- if (_user) {
- _user.username = msgArr[3];
- }
- this.publicEmitter.emit('rename', oldusername!, msgArr[3], selfrename);
- break;
- }
- case 'turn': {
- // Reset all turn data
- for (let user of this.users) user.turn = -1;
- let queuedUsers = parseInt(msgArr[2]);
- if (queuedUsers === 0) {
- this.publicEmitter.emit('turn', {
- user: null,
- queue: [],
- turnTime: null,
- queueTime: null
- });
- return;
- }
- let currentTurn = this.users.find((u) => u.username === msgArr[3])!;
- currentTurn.turn = 0;
- let queue: User[] = [];
- if (queuedUsers > 1) {
- for (let i = 1; i < queuedUsers; i++) {
- let user = this.users.find((u) => u.username === msgArr[i + 3])!;
- queue.push(user);
- user.turn = i;
- }
- }
- this.publicEmitter.emit('turn', {
- user: currentTurn,
- queue: queue,
- turnTime: currentTurn.username === this.username ? parseInt(msgArr[1]) : null,
- queueTime: queue.some((u) => u.username === this.username) ? parseInt(msgArr[msgArr.length - 1]) : null
- });
- break;
- }
- case 'vote': {
- switch (msgArr[1]) {
- case '0':
- // Vote started
- case '1':
- // Vote updated
- let timeToEnd = parseInt(msgArr[2]);
- let yesVotes = parseInt(msgArr[3]);
- let noVotes = parseInt(msgArr[4]);
- // Some server implementations dont send data for status 0, and some do
- if (Number.isNaN(timeToEnd) || Number.isNaN(yesVotes) || Number.isNaN(noVotes)) return;
- this.voteStatus = {
- timeToEnd: timeToEnd,
- yesVotes: yesVotes,
- noVotes: noVotes
- };
- this.publicEmitter.emit('vote', this.voteStatus);
- break;
- case '2':
- // Vote ended
- this.voteStatus = null;
- this.publicEmitter.emit('voteend');
- break;
- case '3':
- // Cooldown
- this.publicEmitter.emit('votecd', parseInt(msgArr[2]));
- break;
- }
- }
- // auth stuff
- case 'auth': {
- this.publicEmitter.emit('auth', msgArr[1]);
- this.auth = true;
- break;
- }
- case 'login': {
- if (msgArr[1] === "1") {
- this.rank = Rank.Registered;
- this.publicEmitter.emit('login', Rank.Registered, new Permissions(0));
- }
- this.publicEmitter.emit('accountlogin', msgArr[1] === "1");
- break;
- }
- case 'admin': {
- switch (msgArr[1]) {
- case '0': {
- // Login
- switch (msgArr[2]) {
- case '0':
- this.publicEmitter.emit('badpw');
- return;
- case '1':
- this.perms.set(65535);
- this.rank = Rank.Admin;
- break;
- case '3':
- this.perms.set(parseInt(msgArr[3]));
- this.rank = Rank.Moderator;
- break;
- }
- this.publicEmitter.emit('login', this.rank, this.perms);
- break;
- }
- case '19': {
- // IP
- this.internalEmitter.emit('ip', msgArr[2], msgArr[3]);
- break;
- }
- case '2': {
- // QEMU
- this.internalEmitter.emit('qemu', msgArr[2]);
- break;
- }
- }
- }
- }
- }
-
- private onWindowResize(e: Event) {
- if (!this.connectedToVM) return;
- // If the canvas is the same size as the screen, don't bother redrawing
- if (window.innerWidth >= this.actualScreenSize.width && this.canvas.width === this.actualScreenSize.width) return;
- if (this.actualScreenSize.width === this.canvasScale.width && this.actualScreenSize.height === this.canvasScale.height) {
- this.unscaledCtx.drawImage(this.canvas, 0, 0);
- }
- this.recalculateCanvasScale(this.actualScreenSize.width, this.actualScreenSize.height);
- this.canvas.width = this.canvasScale.width;
- this.canvas.height = this.canvasScale.height;
- this.ctx.drawImage(this.unscaledCanvas, 0, 0, this.actualScreenSize.width, this.actualScreenSize.height, 0, 0, this.canvas.width, this.canvas.height);
- }
-
- private recalculateCanvasScale(width: number, height: number) {
- this.actualScreenSize.width = width;
- this.actualScreenSize.height = height;
- // If the screen is bigger than the canvas, don't downscale
- if (window.innerWidth >= this.actualScreenSize.width) {
- this.canvasScale.width = this.actualScreenSize.width;
- this.canvasScale.height = this.actualScreenSize.height;
- } else {
- // If the canvas is bigger than the screen, downscale
- this.canvasScale.width = window.innerWidth;
- this.canvasScale.height = (window.innerWidth / this.actualScreenSize.width) * this.actualScreenSize.height;
- }
- }
-
- async WaitForOpen() {
- return new Promise((res) => {
- // TODO: should probably reject on close
- let unsub = this.onInternal('open', () => {
- unsub();
- res();
- });
- });
- }
-
- // Sends a message to the server
- send(...args: StringLike[]) {
- let guacElements = [...args].map((el) => {
- // This catches cases where the thing already is a string
- if (typeof el == 'string') return el as string;
- return el.toString();
- });
-
- this.socket.send(Guacutils.encode(...guacElements));
- }
-
- // Get a list of all VMs
- list(): Promise {
- return new Promise((res, rej) => {
- let u = this.onInternal('list', (list: string[]) => {
- u();
- let vms: VM[] = [];
- for (let i = 0; i < list.length; i += 3) {
- let th = new Image();
- th.src = 'data:image/jpeg;base64,' + list[i + 2];
- vms.push({
- url: this.url,
- id: list[i],
- displayName: list[i + 1],
- thumbnail: th
- });
- }
- res(vms);
- });
- this.send('list');
- });
- }
-
- // Connect to a node
- connect(id: string, username: string | null = null): Promise {
- return new Promise((res) => {
- let u = this.onInternal('connect', (success: boolean) => {
- u();
- res(success);
- });
- if (username === null) this.send('rename');
- else this.send('rename', username);
- this.send('connect', id);
- this.node = id;
- });
- }
-
- // Close the connection
- close() {
- this.connectedToVM = false;
-
- // call all unsubscribe callbacks explicitly
- for (let cb of this.unsubscribeCallbacks) {
- cb();
- }
- this.unsubscribeCallbacks = [];
-
- if (this.socket.readyState === WebSocket.OPEN) this.socket.close();
- }
-
- // Get users
- getUsers(): User[] {
- // Return a copy of the array
- return this.users.slice();
- }
-
- // Send a chat message
- chat(message: string) {
- this.send('chat', message);
- }
-
- // Rename
- rename(username: string | null = null) {
- if (username) this.send('rename', username);
- else this.send('rename');
- }
-
- // Take or drop turn
- turn(taketurn: boolean) {
- this.send('turn', taketurn ? '1' : '0');
- }
-
- // Send mouse instruction
- sendmouse(_x: number, _y: number, mask: number) {
- let x = Math.round((_x / this.canvas.width) * this.actualScreenSize.width);
- let y = Math.round((_y / this.canvas.height) * this.actualScreenSize.height);
- this.send('mouse', x, y, mask);
- }
-
- // Send key
- key(keysym: number, down: boolean) {
- this.send('key', keysym, down ? '1' : '0');
- }
-
- // Get vote status
- getVoteStatus(): VoteStatus | null {
- return this.voteStatus;
- }
-
- // Start a vote, or vote
- vote(vote: boolean) {
- this.send('vote', vote ? '1' : '0');
- }
-
- // Try to login using the specified password
- login(password: string) {
- this.send('admin', AdminOpcode.Login, password);
- }
-
- /* Admin commands */
-
- // Restore
- restore() {
- if (!this.node) return;
- this.send('admin', AdminOpcode.Restore, this.node!);
- }
-
- // Reboot
- reboot() {
- if (!this.node) return;
- this.send('admin', AdminOpcode.Reboot, this.node!);
- }
-
- // Clear turn queue
- clearQueue() {
- if (!this.node) return;
- this.send('admin', AdminOpcode.ClearTurns, this.node!);
- }
-
- // Bypass turn
- bypassTurn() {
- this.send('admin', AdminOpcode.BypassTurn);
- }
-
- // End turn
- endTurn(user: string) {
- this.send('admin', AdminOpcode.EndTurn, user);
- }
-
- // Ban
- ban(user: string) {
- this.send('admin', AdminOpcode.BanUser, user);
- }
-
- // Kick
- kick(user: string) {
- this.send('admin', AdminOpcode.KickUser, user);
- }
-
- // Rename user
- renameUser(oldname: string, newname: string) {
- this.send('admin', AdminOpcode.RenameUser, oldname, newname);
- }
-
- // Mute user
- mute(user: string, state: MuteState) {
- this.send('admin', AdminOpcode.MuteUser, user, state);
- }
-
- // Grab IP
- getip(user: string) {
- if (this.users.find((u) => u.username === user) === undefined) return false;
- return new Promise((res) => {
- let unsubscribe = this.onInternal('ip', (username: string, ip: string) => {
- if (username !== user) return;
- unsubscribe();
- res(ip);
- });
- this.send('admin', AdminOpcode.GetIP, user);
- });
- }
-
- // QEMU Monitor
- qemuMonitor(cmd: string) {
- return new Promise((res) => {
- let unsubscribe = this.onInternal('qemu', (output) => {
- unsubscribe();
- res(output);
- });
- this.send('admin', AdminOpcode.MonitorCommand, this.node!, cmd);
- });
- }
-
- // XSS
- xss(msg: string) {
- this.send('admin', AdminOpcode.ChatXSS, msg);
- }
-
- // Force vote
- forceVote(result: boolean) {
- this.send('admin', AdminOpcode.ForceVote, result ? '1' : '0');
- }
-
- // Toggle turns
- turns(enabled: boolean) {
- this.send('admin', AdminOpcode.ToggleTurns, enabled ? '1' : '0');
- }
-
- // Indefinite turn
- indefiniteTurn() {
- this.send('admin', AdminOpcode.IndefiniteTurn);
- }
-
- // Hide screen
- hideScreen(hidden: boolean) {
- this.send('admin', AdminOpcode.HideScreen, hidden ? '1' : '0');
- }
-
- // Login to account
- loginAccount(token: string) {
- this.send('login', token);
- }
-
- usesAccountAuth() {
- return this.auth;
- }
-
- getNode() {
- return this.node;
- }
-
- private onInternal(event: E, callback: CollabVMClientPrivateEvents[E]): Unsubscribe {
- return this.internalEmitter.on(event, callback);
- }
-
- on(event: E, callback: CollabVMClientEvents[E]): Unsubscribe {
- let unsub = this.publicEmitter.on(event, callback);
- this.unsubscribeCallbacks.push(unsub);
- return unsub;
- }
-}
diff --git a/src/ts/protocol/Guacutils.ts b/src/ts/protocol/Guacutils.ts
deleted file mode 100644
index ee1f310..0000000
--- a/src/ts/protocol/Guacutils.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export function decode(string: string): string[] {
- let pos = -1;
- let sections = [];
-
- for (;;) {
- let len = string.indexOf('.', pos + 1);
-
- if (len === -1) break;
-
- pos = parseInt(string.slice(pos + 1, len)) + len + 1;
-
- // don't allow funky protocol length
- if (pos > string.length) return [];
-
- sections.push(string.slice(len + 1, pos));
-
- const sep = string.slice(pos, pos + 1);
-
- if (sep === ',') continue;
- else if (sep === ';') break;
- // Invalid data.
- else return [];
- }
-
- return sections;
-}
-
-export function encode(...string: string[]): string {
- let command = '';
-
- for (let i = 0; i < string.length; i++) {
- let current = string[i];
- command += current.toString().length + '.' + current;
- command += i < string.length - 1 ? ',' : ';';
- }
- return command;
-}
diff --git a/src/ts/protocol/MuteState.ts b/src/ts/protocol/MuteState.ts
deleted file mode 100644
index 98d6454..0000000
--- a/src/ts/protocol/MuteState.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-enum MuteState {
- Temp = 0,
- Perma = 1,
- Unmuted = 2
-}
-
-export default MuteState;
diff --git a/src/ts/protocol/Permissions.ts b/src/ts/protocol/Permissions.ts
deleted file mode 100644
index 5fbb29c..0000000
--- a/src/ts/protocol/Permissions.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-export class Permissions {
- restore: boolean = false;
- reboot: boolean = false;
- ban: boolean = false;
- forcevote: boolean = false;
- mute: boolean = false;
- kick: boolean = false;
- bypassturn: boolean = false;
- rename: boolean = false;
- grabip: boolean = false;
- xss: boolean = false;
-
- constructor(mask: number) {
- this.set(mask);
- }
-
- set(mask: number) {
- this.restore = (mask & 1) !== 0;
- this.reboot = (mask & 2) !== 0;
- this.ban = (mask & 4) !== 0;
- this.forcevote = (mask & 8) !== 0;
- this.mute = (mask & 16) !== 0;
- this.kick = (mask & 32) !== 0;
- this.bypassturn = (mask & 64) !== 0;
- this.rename = (mask & 128) !== 0;
- this.grabip = (mask & 256) !== 0;
- this.xss = (mask & 512) !== 0;
- }
-}
-
-export enum Rank {
- Unregistered = 0,
- Registered = 1,
- Admin = 2,
- Moderator = 3
-}
-
-// All used admin opcodes as a enum
-export enum AdminOpcode {
- Login = 2,
- MonitorCommand = 5,
- Restore = 8,
- Reboot = 10,
- BanUser = 12,
- ForceVote = 13,
- MuteUser = 14,
- KickUser = 15,
- EndTurn = 16,
- ClearTurns = 17,
- RenameUser = 18,
- GetIP = 19,
- BypassTurn = 20,
- ChatXSS = 21,
- ToggleTurns = 22,
- IndefiniteTurn = 23,
- HideScreen = 24
-}
diff --git a/src/ts/protocol/TurnStatus.ts b/src/ts/protocol/TurnStatus.ts
deleted file mode 100644
index a39b6ac..0000000
--- a/src/ts/protocol/TurnStatus.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { User } from './User.js';
-
-export default interface TurnStatus {
- // The user currently taking their turn
- user: User | null;
- // The users in the turn queue
- queue: User[];
- // Amount of time left in the turn. Null unless the user is taking their turn
- turnTime: number | null;
- // Amount of time until the user gets their turn. Null unless the user is in the queue
- queueTime: number | null;
-}
diff --git a/src/ts/protocol/User.ts b/src/ts/protocol/User.ts
deleted file mode 100644
index a191276..0000000
--- a/src/ts/protocol/User.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Rank } from './Permissions.js';
-
-export class User {
- username: string;
- rank: Rank;
- // -1 means not in the turn queue, 0 means the current turn, anything else is the position in the queue
- turn: number;
-
- constructor(username: string, rank: Rank = Rank.Unregistered) {
- this.username = username;
- this.rank = rank;
- this.turn = -1;
- }
-}
diff --git a/src/ts/protocol/VM.ts b/src/ts/protocol/VM.ts
deleted file mode 100644
index 2c6dfed..0000000
--- a/src/ts/protocol/VM.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export default interface VM {
- url: string;
-
- id: string;
-
- displayName: string;
-
- thumbnail: HTMLImageElement;
-}
diff --git a/src/ts/protocol/VoteStatus.ts b/src/ts/protocol/VoteStatus.ts
deleted file mode 100644
index 480ce02..0000000
--- a/src/ts/protocol/VoteStatus.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export default interface VoteStatus {
- timeToEnd: number;
- yesVotes: number;
- noVotes: number;
-}
diff --git a/src/ts/protocol/mouse.ts b/src/ts/protocol/mouse.ts
deleted file mode 100644
index cbfb110..0000000
--- a/src/ts/protocol/mouse.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-function maskContains(mask: number, bit: number): boolean {
- return (mask & bit) == bit;
-}
-
-export default class Mouse {
- left: boolean = false;
- middle: boolean = false;
- right: boolean = false;
- scrollDown: boolean = false;
- scrollUp: boolean = false;
- x: number = 0;
- y: number = 0;
- constructor() {}
-
- makeMask() {
- var mask = 0;
- if (this.left) mask |= 1;
- if (this.middle) mask |= 2;
- if (this.right) mask |= 4;
- if (this.scrollUp) mask |= 8;
- if (this.scrollDown) mask |= 16;
- return mask;
- }
-
- initFromMouseEvent(e: MouseEvent) {
- this.left = maskContains(e.buttons, 1);
- this.right = maskContains(e.buttons, 2);
- this.middle = maskContains(e.buttons, 4);
-
- this.x = e.offsetX;
- this.y = e.offsetY;
- }
-
- // don't think there's a good way of shoehorning this in processEvent so ..
- // (I guess could union e to be MouseEvent|WheelEvent and put this in there, but it'd be a
- // completely unnesscary runtime check for a one-event situation, so having it be seperate
- // and even call the MouseEvent implementation is more than good enough)
- initFromWheelEvent(ev: WheelEvent) {
- this.initFromMouseEvent(ev as MouseEvent);
-
- // Now do the actual wheel handling
- if (ev.deltaY < 0) this.scrollUp = true;
- else if (ev.deltaY > 0) this.scrollDown = true;
- }
-}
diff --git a/src/ts/tests/format.test.ts b/src/ts/tests/format.test.ts
deleted file mode 100644
index 716970b..0000000
--- a/src/ts/tests/format.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Format } from '../format';
-
-test('a string without any format specifiers in it is unaltered', () => {
- expect(Format('Hello World')).toBe('Hello World');
-});
-
-test('formatting a string works', () => {
- expect(Format('Hello, {0}!', 'World')).toBe('Hello, World!');
-});
-
-test('a cut off format specifier throws', () => {
- expect(() => Format('a{0', 1)).toThrow('Cutoff/invalid format specifier');
-});
-
-test('a malformed format specifier throws', () => {
- expect(() => Format('a{-0}', 1)).toThrow('Malformed format specifier');
- expect(() => Format('a{0-}', 1)).toThrow('Malformed format specifier');
- expect(() => Format('a{0ab}', 1)).toThrow('Malformed format specifier');
- expect(() => Format('a{ab0ab}', 1)).toThrow('Malformed format specifier');
-
- // Whitespace is not permitted inside a format specifier
- expect(() => Format('a{0 }', 1)).toThrow('Whitespace inside format specifier');
- expect(() => Format('a{ 0}', 1)).toThrow('Whitespace inside format specifier');
- expect(() => Format('a{ 0 }', 1)).toThrow('Whitespace inside format specifier');
-});
-
-test("a OOB format specifier doesn't work", () => {
- expect(() => Format('a {37}', 1)).toThrow('Argument index out of bounds');
-});