Delete src directory cuz u suck @everyone

This commit is contained in:
cvmfucker 2024-04-17 20:26:00 +03:00 committed by GitHub
parent 42265ab184
commit 6c38ad997a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 0 additions and 4164 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@ -1,342 +0,0 @@
#vmview {
display: none;
padding-right: 0 !important;
padding-left: 0 !important;
}
/*.vmtile {
text-decoration: none;
color: #FFFFFF;
font-size: 16pt;
border: 2px solid #575757;
border-radius: 15px;
height: fit-content;
width: fit-content;
display: block;
padding: 4px;
}*/
#vmDisplay, #btns {
text-align: center;
display: block;
margin-bottom: 10px;
}
#vmlist > div.row > div {
padding-bottom: 10px;
}
#vmlist div.col-md-3 > div.card:hover {
cursor: pointer;
border-color: rgb(8, 121, 250);
}
.vmtile > img {
margin-bottom: 2px;
}
.chat-table, .username-table {
overflow-y: auto;
border: 1px solid #575757;
}
.chat-table {
height: 30vh;
}
.username-table {
max-height: 30vh;
}
.username-table > table > thead {
position: sticky;
top: 0;
}
#turnstatus {
text-align: center;
}
#voteResetPanel {
text-align: center;
}
.focused {
box-shadow: 0 0 9px 0 rgba(45,213,255,.75);
-moz-box-shadow: 0 0 9px 0 rgba(45,213,255,.75);
-webkit-box-shadow: 0 0 9px 0 rgba(45,213,255,.75)
}
.waiting {
box-shadow: 0 0 9px 0 rgba(242,255,63,.75);
-moz-box-shadow: 0 0 9px 0 rgba(242,255,63,.75);
-webkit-box-shadow: 0 0 9px 0 rgba(242,255,63,.75)
}
#staffbtns {
display: none;
}
#staffbtns > button {
display: none;
}
#qemuMonitorOutput {
height: 180px;
}
#xssCheckboxContainer {
display: none;
}
#forceVotePanel {
display: none;
}
tr.user-admin > td, .chat-username-admin, .username-admin {
color: #FF0000 !important;
}
tr.user-moderator > td, .chat-username-moderator, .username-moderator {
color: #00FF00 !important;
}
html[data-bs-theme="dark"] {
tr.user-unregistered > td, .chat-username-unregistered, .username-unregistered {
color: #b1b1b1 !important;
}
tr.user-registered > td, .chat-username-registered, .username-registered {
color: #FFFFFF !important;
}
tr.user-registered.user-turn > td, tr.user-registered.user-waiting > td {
color: #000000 !important;
--bs-table-color: #000000 !important;
}
tr.user-unregistered.user-turn > td, tr.user-unregistered.user-waiting > td {
color: #585858 !important;
--bs-table-color: #585858 !important;
}
}
html[data-bs-theme="light"] {
tr.user-unregistered > td, .chat-username-unregistered, .username-unregistered {
color: #6b6b6b !important;
}
tr.user-registered > td, .chat-username-registered, .username-registered {
color: #000 !important;
}
}
tr.user-turn > td {
background-color: #cfe2ff !important;
--bs-table-bg-state: #cfe2ff !important;
color: #000000;
--bs-table-color: #000000;
}
tr.user-turn:hover, tr.user-turn > td:hover {
background-color: #bacbe6 !important;
--bs-table-bg-state: #bacbe6 !important;
}
tr.user-waiting > td {
background-color: #fff3cd !important;
--bs-table-bg-state: #fff3cd !important;
color: #000000;
--bs-table-color: #000000;
}
.tr.user-waiting:hover, tr.user-waiting > td:hover {
background-color: #ece1be !important;
--bs-table-bg-state: #ece1be !important;
}
.user-current {
font-style: italic;
}
/* Start OSK */
.osk-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 1024px;
margin: 0 auto;
margin-bottom: 10px;
border-radius: 5px;
}
.simple-keyboard.hg-theme-default {
display: inline-block;
}
.osk-main.simple-keyboard {
max-width: 640px;
background: none;
}
.osk-main.simple-keyboard .hg-row:first-child {
margin-bottom: 8.51px; /* wtf? */
}
.osk-arrows.simple-keyboard {
align-self: flex-end;
background: none;
}
.simple-keyboard .hg-button.selectedButton {
background: rgba(5, 25, 70, 0.53);
color: white;
}
.simple-keyboard .hg-button.emptySpace {
pointer-events: none;
background: none;
border: none;
box-shadow: none;
}
.osk-arrows .hg-row {
justify-content: center;
}
.osk-arrows .hg-button {
width: 50px;
flex-grow: 0 !important;
justify-content: center !important;
display: flex !important;
align-items: center !important;
}
.controlArrows {
display: flex;
align-items: center;
justify-content: space-between;
flex-flow: column;
}
.osk-control.simple-keyboard {
background: none;
}
.osk-control.simple-keyboard .hg-row:first-child {
margin-bottom: 8.51px;
}
.osk-control .hg-button {
width: 50px;
flex-grow: 0 !important;
justify-content: center !important;
display: flex !important;
align-items: center !important;
}
.numPad {
display: flex;
align-items: flex-end;
}
.osk-numpad.simple-keyboard {
background: none;
}
.osk-numpad.simple-keyboard {
width: 160px;
}
.osk-numpad.simple-keyboard .hg-button {
width: 50px;
justify-content: center;
display: flex;
align-items: center;
}
.osk-numpadEnd.simple-keyboard {
width: 50px;
background: none;
margin: 0;
padding: 5px 5px 5px 0;
}
.osk-numpadEnd.simple-keyboard .hg-button {
align-items: center;
justify-content: center;
display: flex;
}
.osk-numpadEnd .hg-button.hg-standardBtn.hg-button-plus {
height: 85px;
}
.osk-numpadEnd.simple-keyboard .hg-button.hg-button-enter {
height: 85px;
}
.simple-keyboard.hg-theme-default .hg-button.hg-selectedButton {
background: rgba(5, 25, 70, 0.53);
color: white;
}
.hg-button.hg-functionBtn.hg-button-space {
width: 350px;
}
@media screen and (max-width: 640px) {
.hg-button:not(:last-child) {
margin-right: 1px !important;
}
}
/*
Theme: cvmDark
*/
.simple-keyboard.cvmDark .hg-button {
border-bottom: none;
background: rgba(0, 0, 0, 0.5);
color: white;
}
.simple-keyboard.cvmDark .hg-button:active {
background: #1c4995;
color: white;
}
#root .simple-keyboard.cvmDark + .simple-keyboard-preview {
background: #1c4995;
}
/*
Theme: cvmDisabled
*/
.simple-keyboard.cvmDisabled .hg-button {
border-bottom: none;
pointer-events: none;
background: gray;
color: white;
}
/* End OSK */
#badPasswordAlert {
display: none;
}
/* NSFW Blur */
.cvm-nsfw {
img {
filter:blur(40px)!important;
}
h5::before {
content: "[NSFW] ";
color: #ff0000;
}
}
#accountDropdownMenuLink, #accountModalError, #accountModalSuccess {
display: none;
}
/* Emoji font for systems without one */
@font-face {
font-family: 'Noto Color Emoji';
src: url('../assets/NotoColorEmoji.ttf');
}

View File

@ -1,309 +0,0 @@
<!DOCTYPE HTML>
<html prefix="og: https://ogp.me/ns#" data-bs-theme="dark">
<head>
<title>CollabVM</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8"/>
<link href="../css/style.css" rel="stylesheet" type="text/css"/>
<link href="../../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
<script src="https://kit.fontawesome.com/7add23c1ae.js" crossorigin="anonymous"></script>
<link rel="icon" href="../assets/favicon.ico">
<meta name="description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
<!-- Opengraph shit -->
<meta property="og:type" content="website"/>
<meta property="og:title" content="CollabVM"/>
<meta property="og:url" content="https://computernewb.com/collab-vm/"/>
<meta property="og:description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
<meta property="og:site_name" content="Computernewb"/>
<meta property="og:image" content="https://computernewb.com/collab-vm/desktop.png"/>
</head>
<body>
<div class="modal fade" id="qemuMonitorModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 id="qemuModalHeader" class="modal-title"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<textarea id="qemuMonitorOutput" readonly="" class="form-control"></textarea>
<div class="input-group">
<input type="text" id="qemuMonitorInput" class="form-control" placeholder="Command"/>
<button class="btn btn-outline-secondary btn-primary" type="button" id="qemuMonitorSendBtn"></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="welcomeModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" aria-labelledby="welcomeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 id="welcomeModalHeader"></h1>
</div>
<div class="modal-body" id="welcomeModalBody"></div>
<div class="modal-footer">
<button type="button" id="welcomeModalDismiss" class="btn btn-primary" data-bs-dismiss="modal"></button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Login</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger alert-dismissible" id="badPasswordAlert" role="alert">
<span id="badPasswordAlertText"></span>
<button type="button" class="btn-close" aria-label="Close" id="incorrectPasswordDismissBtn"></button>
</div>
<div class="input-group">
<input type="hidden" name="username" id="adminInputVMID"/>
<span class="input-group-text" id="loginModalPasswordText"></span>
<input id="adminPassword" type="password" class="form-control" name="password"/>
</div>
</div>
<div class="modal-footer">
<button type="button" id="loginButton" class="btn btn-primary"></button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="accountModal" tabindex="-1" aria-labelledby="accountModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="accountModalTitle"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger alert-dismissible" id="accountModalError" role="alert">
<span id="accountModalErrorText"></span>
<button type="button" class="btn-close" aria-label="Close" id="accountModalErrorDismiss"></button>
</div>
<div class="alert alert-success alert-dismissible" id="accountModalSuccess" role="alert">
<span id="accountModalSuccessText"></span>
<button type="button" class="btn-close" aria-label="Close" id="accountModalSuccessDismiss"></button>
</div>
<div id="accountLoginSection">
<form id="accountLoginForm">
<label for="accountLoginUsername" id="accountLoginUsernameLabel"></label><br/>
<input id="accountLoginUsername" type="text" class="form-control" name="username" required/><br>
<label for="accountLoginPassword" id="accountLoginPasswordLabel"></label><br/>
<input id="accountLoginPassword" type="password" class="form-control" name="password" required><br>
<div id="accountLoginCaptcha"></div>
<button type="submit" class="btn btn-primary" id="accountModalLoginBtn"></button> <button type="button" class="btn btn-secondary" id="accountForgotPasswordButton"></button>
</form>
</div>
<div id="accountRegisterSection">
<form id="accountRegisterForm">
<label for="accountRegisterEmail" id="accountRegisterEmailLabel"></label><br/>
<input id="accountRegisterEmail" type="email" class="form-control" name="email" required/><br>
<label for="accountRegisterUsername" id="accountRegisterUsernameLabel"></label><br/>
<input id="accountRegisterUsername" type="text" class="form-control" name="username" required/><br>
<label for="accountRegisterPassword" id="accountRegisterPasswordLabel"></label><br/>
<input id="accountRegisterPassword" type="password" class="form-control" name="password" required><br>
<label for="accountRegisterConfirmPassword" id="accountRegisterConfirmPasswordLabel"></label><br/>
<input id="accountRegisterConfirmPassword" type="password" class="form-control" name="confirmpassword" required><br>
<label for="accountRegisterDateOfBirth" id="accountRegisterDateOfBirthLabel"></label><br/>
<input id="accountRegisterDateOfBirth" type="date" class="form-control" name="dateofbirth" required><br/>
<div id="accountRegisterCaptcha"></div>
<button type="submit" class="btn btn-primary" id="accountModalRegisterBtn"></button>
</form>
</div>
<div id="accountVerifyEmailSection">
<center>
<i class="fa-solid fa-envelope" style="font-size: 12rem"></i>
<p id="accountVerifyEmailText"></p>
<form id="accountVerifyEmailForm">
<label for="accountVerifyEmailCode" id="accountVerifyEmailCodeLabel"></label><br>
<input id="accountVerifyEmailCode" type="text" class="form-control" name="code" required><br>
<label for="accountVerifyEmailPassword" id="accountVerifyEmailPasswordLabel"></label><br>
<input id="accountVerifyEmailPassword" type="password" class="form-control" name="password" required/><br/>
<button type="submit" class="btn btn-primary" id="accountModalVerifyEmailBtn"></button>
</form>
</center>
</div>
<div id="accountSettingsSection">
<form id="accountSettingsForm">
<label for="accountSettingsEmail" id="accountSettingsEmailLabel"></label><br>
<input id="accountSettingsEmail" type="email" class="form-control" name="email" required/><br/>
<label for="accountSettingsUsername" id="accountSettingsUsernameLabel"></label><br>
<input id="accountSettingsUsername" type="text" class="form-control" name="username" required/><br/>
<label for="accountSettingsNewPassword" id="accountSettingsNewPasswordLabel"></label>
<input id="accountSettingsNewPassword" type="password" class="form-control" name="password"/><br/>
<label for="accountSettingsConfirmNewPassword" id="accountSettingsConfirmNewPasswordLabel"></label>
<input id="accountSettingsConfirmNewPassword" type="password" class="form-control" name="confirmpassword"/><br/>
<label for="accountSettingsCurrentPassword" id="accountSettingsCurrentPasswordLabel"></label>
<input id="accountSettingsCurrentPassword" type="password" class="form-control" name="currentpassword" required/><br/>
<button type="submit" class="btn btn-primary" id="updateAccountSettingsBtn"></button>
</form>
</div>
<div id="accountResetPasswordSection">
<form id="accountResetPasswordForm">
<label for="accountResetPasswordEmail" id="accountResetPasswordEmailLabel"></label><br>
<input id="accountResetPasswordEmail" type="email" class="form-control" name="email" required/><br/>
<label for="accountResetPasswordUsername" id="accountResetPasswordUsernameLabel"></label>
<input id="accountResetPasswordUsername" type="text" class="form-control" name="username" required/><br/>
<div id="accountResetPasswordCaptcha"></div>
<button type="submit" class="btn btn-primary" id="accountResetPasswordBtn"></button>
</div>
<div id="accountResetPasswordVerifySection">
<center>
<i class="fa-solid fa-envelope" style="font-size: 12rem"></i>
<p id="accountVerifyPasswordResetText"></p>
<form id="accountResetPasswordVerifyForm">
<label for="accountResetPasswordCode" id="accountResetPasswordCodeLabel"></label><br>
<input id="accountResetPasswordCode" type="text" class="form-control" name="code" required><br>
<label for="accountResetPasswordNewPassword" id="accountResetPasswordNewPasswordLabel"></label><br>
<input id="accountResetPasswordNewPassword" type="password" class="form-control" name="password" required/><br/>
<label for="accountResetPasswordConfirmNewPassword" id="accountResetPasswordConfirmNewPasswordLabel"></label><br>
<input id="accountResetPasswordConfirmNewPassword" type="password" class="form-control" name="confirmpassword" required/><br/>
<button type="submit" class="btn btn-primary" id="accountResetPasswordVerifyBtn"></button>
</form>
</center>
</div>
</div>
</div>
</div>
</div>
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href="#"><span id="siteNameText"></span></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a id="homeBtn" href="#" class="nav-link active" aria-current="page"><i class="fa-solid fa-house"></i> <span id="homeBtnText"></span></a>
</li>
<li class="nav-item">
<a href="https://computernewb.com/collab-vm/faq/" class="nav-link"><i class="fa-solid fa-circle-question"></i> <span id="faqBtnText"></span></a>
</li>
<li class="nav-item">
<a id="rulesBtn" href="https://computernewb.com/collab-vm/rules" class="nav-link"><i class="fa-solid fa-clipboard-check"></i> <span id="rulesBtnText"></span></a>
</li>
<li class="nav-item">
<a href="https://discord.gg/a4kqb4mGyX" class="nav-link"><i class="fa-brands fa-discord"></i> Discord</a>
</li>
<li class="nav-item">
<a href="https://reddit.com/r/collabvm" class="nav-link"><i class="fa-brands fa-reddit"></i> Subreddit</a>
</li>
<li class="nav-item">
<a rel="me" class="nav-link" href="https://fedi.computernewb.com/@collabvm"><i class="fa-brands fa-mastodon"></i> Mastodon</a>
</li>
<li class="nav-item">
<a href="https://computernewb.com/collab-vm/user-vm" class="nav-link"><i class="fa-solid fa-user"></i> UserVM</a>
</li>
<li class="nav-item">
<a id="toggleThemeBtn" href="#" class="nav-link"><i id="toggleThemeIcon" class="fa-solid fa-sun"></i> <span id="toggleThemeBtnText"></span></a>
</li>
<li class="nav-item dropdown">
<a id="languageDropdownLink" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-globe"></i> <span id="languageDropdownText"></span>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="languageDropdownLink">
<span id="languageDropdown"></span>
<a class="dropdown-item" href="https://github.com/computernewb/collab-vm-1.2-webapp/wiki/Contributing-a-Language" target="_blank"><i class="fa-solid fa-plus"></i> Add Yours</a>
</div>
</li>
</ul>
<div class="navbar-text dropdown">
<a class="nav-link dropdown-toggle" href="#" id="accountDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-user"></i> <span id="accountDropdownUsername"></span>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="accountDropdownMenuLink">
<a class="dropdown-item" href="#" id="accountLoginButton"></a>
<a class="dropdown-item" href="#" id="accountRegisterButton"></a>
<a class="dropdown-item" href="#" id="accountSettingsButton"></a>
<a class="dropdown-item" href="#" id="accountLogoutButton"></a>
</div>
</div>
</div>
</div>
</nav>
<div class="container-fluid" id="vmlist">
<div class="row"></div>
</div>
<div id="vmview">
<div id="vmDisplay"></div>
<p id="turnstatus"></p>
<div id="voteResetPanel" style="display:none;">
<span id="voteResetHeaderText"></span><br/>
<button class="btn btn-success" id="voteYesBtn"><i class="fa-solid fa-check"></i> <span id="voteYesBtnText"></span><span class="badge bg-secondary" id="voteYesLabel"></span></button> <button class="btn btn-danger" id="voteNoBtn"><i class="fa-solid fa-ban"></i> <span id="voteNoBtnText"></span><span class="badge bg-secondary" id="voteNoLabel"></span></button><br/>
<span id="voteTimeText"></span>
<div id="forceVotePanel">
<button class="btn btn-info" id="forceVoteYesBtn"><i class="fa-solid fa-check"></i> <span id="passVoteBtnText"></span></button>
<button class="btn btn-info" id="forceVoteNoBtn"><i class="fa-solid fa-ban"></i> <span id="cancelVoteBtnText"></span></button>
</div>
</div>
<div id="btns">
<button class="btn btn-secondary" id="takeTurnBtn"><i class="fa-solid fa-computer-mouse"></i> <span id="turnBtnText"></span></button>
<button class="btn btn-secondary" id="oskBtn"><i class="fa-solid fa-keyboard"></i> <span id="oskBtnText"></span></button>
<button class="btn btn-secondary" id="changeUsernameBtn"><i class="fa-solid fa-signature"></i> <span id="changeUsernameBtnText"></span></button>
<button class="btn btn-secondary" id="voteResetButton"><i class="fa-solid fa-rotate-left"></i> <span id="voteForResetBtnText"></span></button>
<button class="btn btn-secondary" id="screenshotButton"><i class="fa-solid fa-camera"></i> <span id="screenshotBtnText"></span></button>
<button class="btn btn-secondary" id="ctrlAltDelBtn"><i class="fa-solid fa-gear"></i> <span id="ctrlAltDelBtnText"></span></button>
<div id="staffbtns">
<button class="btn btn-secondary" id="restoreBtn"><i class="fa-solid fa-rotate-left"></i> <span id="restoreBtnText"></span></button>
<button class="btn btn-secondary" id="rebootBtn"><i class="fa-solid fa-power-off"></i> <span id="rebootBtnText"></span></button>
<button class="btn btn-secondary" id="clearQueueBtn"><i class="fa-solid fa-eraser"></i> <span id="clearQueueBtnText"></span></button>
<button class="btn btn-secondary" id="bypassTurnBtn"><i class="fa-solid fa-forward"></i> <span id="bypassTurnBtnText"></span></button>
<button class="btn btn-secondary" id="endTurnBtn"><i class="fa-solid fa-ban"></i> <span id="endTurnBtnText"></span></button>
<button class="btn btn-secondary" id="indefTurnBtn"><i class="fa-solid fa-infinity"></i> <span id="indefTurnBtnText"></span></button>
<button class="btn btn-secondary" id="qemuMonitorBtn" data-bs-toggle="modal" data-bs-target="#qemuMonitorModal"><i class="fa-solid fa-terminal"></i> <span id="qemuMonitorBtnText"></span></button>
</div>
</div>
<div class="osk-container d-none" id="osk-container">
<div class="osk-main"></div>
<div class="controlArrows">
<div class="osk-control"></div>
<div class="osk-arrows"></div>
</div>
<div class="numPad">
<div class="osk-numpad"></div>
<div class="osk-numpadEnd"></div>
</div>
</div>
<div class="row container-fluid">
<div class="col-md-4">
<div class="table-responsive username-table">
<table class="table table-hover table-borderless">
<thead>
<th class="bg-body-tertiary"><i class="fa-solid fa-user"></i> <span id="usersOnlineText"></span> (<span id="onlineusercount"></span>)</th>
</thead>
<tbody id="userlist"></tbody>
</table>
</div>
</div>
<div class="col-md-8">
<div class="table-responsive chat-table" id="chatListDiv">
<table class="table table-hover table-borderless">
<tbody id="chatList">
</tbody>
</table>
</div>
<div class="input-group">
<span class="input-group-text username-unregistered" id="username"></span>
<input type="text" class="form-control" id="chat-input"/>
<div class="input-group-text" id="xssCheckboxContainer">
<input class="form-check-input" type="checkbox" value="" id="xssCheckbox"/>
<label class="form-check-label" for="xssCheckbox">XSS</label>
</div>
<button class="btn btn-primary" type="button" id="sendChatBtn"><i class="fa-solid fa-paper-plane"></i></button>
</div>
</div>
</div>
</div>
<script src="https://js.hcaptcha.com/1/api.js"></script>
<script type="module" src="../ts/main.ts" type="application/javascript"></script>
</body>
</html>

View File

@ -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<AuthServerInformation> {
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<AccountLoginResult> {
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<SessionResult>(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<AccountRegisterResult> {
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<LogoutResult>(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<VerifyEmailResult>(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<UpdateAccountResult>(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<PasswordResetResult>(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<PasswordResetResult>(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;
}

View File

@ -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;

View File

@ -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": "<p>Before continuing, please familiarize yourself with our rules:</p> <h3>R1. Don't break the law.</h3> 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.<br><br>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.<br><br>COPPA is also enforced, please do not use CollabVM if you are under the age of 13 years old. <h3>R2. No running DoS/DDoS tools.</h3> Do not use CollabVM to DoS/DDoS an indivdiual, business, company, or anyone else. <h3>R3. No spam distribution.</h3> Do not spam any emails using this service or push spam in general. <h3>R4. Do not abuse any exploits.</h3> 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 <h3>R5. Don't impersonate other users.</h3> Do not impersonate other members of CollabVM. If caught, you'll be temporarily disconnected, and banned if necessary. <h3>R6. One vote per person.</h3> 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. <h3>R7. No Remote Administration Tools.</h3> Do not use any remote administration tools (ex: DarkComet, NanoCore, Anydesk, TeamViewer, Orcus, etc.) <h3>R8. No bypassing CollabNet.</h3> 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). <h3>R9. No performing destructive actions constantly.</h3> 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\"). <h3>R10. No Cryptomining</h3> 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. <h3>NSFW Warning</h3> 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;

View File

@ -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<StringLike>) {
let argumentsAsStrings: Array<string> = [...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;
}

View File

@ -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<string>;
// 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<string, Language> = new Map<string, Language>();
private lang: Language = fallbackLanguage;
private languageDropdown: HTMLSpanElement = document.getElementById('languageDropdown') as HTMLSpanElement;
private emitter: Emitter<I18nEvents> = 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<e extends keyof I18nEvents>(event: e, cb: I18nEvents[e]): Unsubscribe {
return this.emitter.on(event, cb);
}
}
export let TheI18n = new I18n();

View File

@ -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]
];

File diff suppressed because it is too large Load Diff

View File

@ -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<CollabVMClientPrivateEvents>;
// public events
private publicEmitter: Emitter<CollabVMClientEvents>;
private unsubscribeCallbacks: Array<Unsubscribe> = [];
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<void>((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<VM[]> {
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<boolean> {
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<string>((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<string>((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<E extends keyof CollabVMClientPrivateEvents>(event: E, callback: CollabVMClientPrivateEvents[E]): Unsubscribe {
return this.internalEmitter.on(event, callback);
}
on<E extends keyof CollabVMClientEvents>(event: E, callback: CollabVMClientEvents[E]): Unsubscribe {
let unsub = this.publicEmitter.on(event, callback);
this.unsubscribeCallbacks.push(unsub);
return unsub;
}
}

View File

@ -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;
}

View File

@ -1,7 +0,0 @@
enum MuteState {
Temp = 0,
Perma = 1,
Unmuted = 2
}
export default MuteState;

View File

@ -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
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -1,9 +0,0 @@
export default interface VM {
url: string;
id: string;
displayName: string;
thumbnail: HTMLImageElement;
}

View File

@ -1,5 +0,0 @@
export default interface VoteStatus {
timeToEnd: number;
yesVotes: number;
noVotes: number;
}

View File

@ -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;
}
}

View File

@ -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');
});