Files
collabvm-1.2.ts/cvmts/src/CollabVMServer.ts

1006 lines
30 KiB
TypeScript
Raw Normal View History

import IConfig from './IConfig.js';
import * as Utilities from './Utilities.js';
import { User, Rank } from './User.js';
// I hate that you have to do it like this
import CircularBuffer from 'mnemonist/circular-buffer.js';
import Queue from 'mnemonist/queue.js';
2023-01-31 22:00:30 -05:00
import { createHash } from 'crypto';
import { VMState, QemuVM, QemuVmDefinition } from '@computernewb/superqemu';
2024-06-11 13:46:24 -04:00
import { IPDataManager } from './IPData.js';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import AuthManager from './AuthManager.js';
import { JPEGEncoder } from './JPEGEncoder.js';
import VM from './vm/interface.js';
2024-06-23 02:23:59 -04:00
import { ReaderModel } from '@maxmind/geoip2-node';
import { Size, Rect } from './Utilities.js';
import pino from 'pino';
import { BanManager } from './BanManager.js';
import { IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, TheProtocolManager } from './protocol/Protocol.js';
// Instead of strange hacks we can just use nodejs provided
// import.meta properties, which have existed since LTS if not before
const __dirname = import.meta.dirname;
const kCVMTSAssetsRoot = path.resolve(__dirname, '../../assets');
const kRestartTimeout = 5000;
type ChatHistory = {
2024-04-24 03:50:17 -04:00
user: string;
msg: string;
};
type VoteTally = {
yes: number;
no: number;
};
export default class CollabVMServer implements IProtocolHandlers {
2024-04-24 03:50:17 -04:00
private Config: IConfig;
2024-04-24 03:50:17 -04:00
private clients: User[];
private ChatHistory: CircularBuffer<ChatHistory>;
private TurnQueue: Queue<User>;
// Time remaining on the current turn
private TurnTime: number;
// Interval to keep track of the current turn time
private TurnInterval?: NodeJS.Timeout;
// If a reset vote is in progress
private voteInProgress: boolean;
// Interval to keep track of vote resets
private voteInterval?: NodeJS.Timeout;
// How much time is left on the vote
private voteTime: number;
// How much time until another reset vote can be cast
private voteCooldown: number;
// Interval to keep track
private voteCooldownInterval?: NodeJS.Timeout;
// Completely disable turns
private turnsAllowed: boolean;
// Hide the screen
private screenHidden: boolean;
// base64 image to show when the screen is hidden
private screenHiddenImg: Buffer;
private screenHiddenThumb: Buffer;
2024-04-24 03:50:17 -04:00
// Indefinite turn
private indefiniteTurn: User | null;
private ModPerms: number;
2024-06-11 13:46:24 -04:00
private VM: VM;
2024-04-24 03:50:17 -04:00
// Authentication manager
private auth: AuthManager | null;
2024-06-23 02:23:59 -04:00
// Geoip
private geoipReader: ReaderModel | null;
// Ban manager
private banmgr: BanManager;
// queue of rects, reset every frame
private rectQueue: Rect[] = [];
private logger = pino({ name: 'CVMTS.Server' });
2024-04-24 03:50:17 -04:00
constructor(config: IConfig, vm: VM, banmgr: BanManager, auth: AuthManager | null, geoipReader: ReaderModel | null) {
2024-04-24 03:50:17 -04:00
this.Config = config;
this.ChatHistory = new CircularBuffer<ChatHistory>(Array, this.Config.collabvm.maxChatHistoryLength);
this.TurnQueue = new Queue<User>();
this.TurnTime = 0;
this.clients = [];
this.voteInProgress = false;
this.voteTime = 0;
this.voteCooldown = 0;
this.turnsAllowed = true;
this.screenHidden = false;
this.screenHiddenImg = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhidden.jpeg'));
this.screenHiddenThumb = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhiddenthumb.jpeg'));
2024-04-24 03:50:17 -04:00
this.indefiniteTurn = null;
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
2024-07-11 20:49:49 -04:00
// No size initially, since there usually won't be a display connected at all during initalization
this.OnDisplayResized({
2024-04-24 03:50:17 -04:00
width: 0,
height: 0
});
2024-04-24 03:50:17 -04:00
this.VM = vm;
let self = this;
vm.Events().on('statechange', (newState: VMState) => {
if (newState == VMState.Started) {
self.logger.info('VM started');
// start the display
if (self.VM.GetDisplay() == null) {
self.VM.StartDisplay();
}
self.VM.GetDisplay()?.on('connected', () => {
// well aware this sucks but whatever
self.VM.GetDisplay()?.on('resize', (size: Size) => self.OnDisplayResized(size));
self.VM.GetDisplay()?.on('rect', (rect: Rect) => self.OnDisplayRectangle(rect));
self.VM.GetDisplay()?.on('frame', () => self.OnDisplayFrame());
});
}
if (newState == VMState.Stopped) {
setTimeout(async () => {
self.logger.info('restarting VM');
await self.VM.Start();
}, kRestartTimeout);
}
});
2024-04-24 03:50:17 -04:00
// authentication manager
this.auth = auth;
2024-06-23 02:23:59 -04:00
this.geoipReader = geoipReader;
this.banmgr = banmgr;
2024-04-24 03:50:17 -04:00
}
public connectionOpened(user: User) {
let sameip = this.clients.filter((c) => c.IP.address === user.IP.address);
if (sameip.length >= this.Config.collabvm.maxConnections) {
// Kick the oldest client
// I think this is a better solution than just rejecting the connection
sameip[0].kick();
}
2024-04-24 03:50:17 -04:00
this.clients.push(user);
2024-06-23 02:23:59 -04:00
if (this.Config.geoip.enabled) {
try {
user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode;
} catch (error) {
this.logger.warn(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`);
2024-06-23 02:23:59 -04:00
}
}
user.socket.on('msg', (buf: Buffer, binary: boolean) => {
try {
user.protocol.processMessage(buf);
} catch (err) {
user.kick();
}
});
user.socket.on('disconnect', () => this.connectionClosed(user));
// Set ourselves as the handler
user.protocol.setHandler(this as IProtocolHandlers);
2024-04-24 03:50:17 -04:00
if (this.Config.auth.enabled) {
user.protocol.sendAuth(this.Config.auth.apiEndpoint);
2024-04-24 03:50:17 -04:00
}
user.protocol.sendAddUser(this.getAddUser());
if (this.Config.geoip.enabled) {
let flags = this.getFlags();
user.protocol.sendFlag(flags);
}
2024-04-24 03:50:17 -04:00
}
private connectionClosed(user: User) {
let clientIndex = this.clients.indexOf(user);
if (clientIndex === -1) return;
if (user.IP.vote != null) {
user.IP.vote = null;
this.sendVoteUpdate();
}
if (this.indefiniteTurn === user) this.indefiniteTurn = null;
this.clients.splice(clientIndex, 1);
user.protocol.dispose();
this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
2024-04-24 03:50:17 -04:00
if (!user.username) return;
if (this.TurnQueue.toArray().indexOf(user) !== -1) {
var hadturn = this.TurnQueue.peek() === user;
this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((u) => u !== user));
if (hadturn) this.nextTurn();
}
this.clients.forEach((c) => c.protocol.sendRemUser([user.username!]));
2024-04-24 03:50:17 -04:00
}
// IProtocolHandlers
// does auth check
private authCheck(user: User, guestPermission: boolean) {
if (!this.Config.auth.enabled) return true;
if (user.rank === Rank.Unregistered && !guestPermission) {
user.protocol.sendChatMessage('', 'You need to login to do that.');
return false;
}
return true;
}
onNop(user: User): void {
user.onNop();
}
async onLogin(user: User, token: string) {
if (!this.Config.auth.enabled) return;
if (!user.connectedToNode) {
user.protocol.sendLoginResponse(false, 'You must connect to the VM before logging in.');
return;
}
try {
let res = await this.auth!.Authenticate(token, user);
if (res.clientSuccess) {
this.logger.info(`${user.IP.address} logged in as ${res.username}`);
user.protocol.sendLoginResponse(true, '');
let old = this.clients.find((c) => c.username === res.username);
if (old) {
// kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that
// so we call connectionClosed manually here. When it gets called on kick(), it will return because the user isn't in the list
this.connectionClosed(old);
await old.kick();
2024-06-23 02:23:59 -04:00
}
// Set username
if (user.countryCode !== null && user.noFlag) {
// privacy
for (let cl of this.clients.filter((c) => c !== user)) {
cl.protocol.sendRemUser([user.username!]);
}
this.renameUser(user, res.username, false);
} else this.renameUser(user, res.username, true);
// Set rank
user.rank = res.rank;
if (user.rank === Rank.Admin) {
user.protocol.sendAdminLoginResponse(true, undefined);
} else if (user.rank === Rank.Moderator) {
user.protocol.sendAdminLoginResponse(true, this.ModPerms);
}
this.clients.forEach((c) =>
c.protocol.sendAddUser([
{
username: user.username!,
rank: user.rank
}
])
);
} else {
user.protocol.sendLoginResponse(false, res.error!);
if (res.error === 'You are banned') {
user.kick();
}
}
} catch (err) {
this.logger.error(`Error authenticating client ${user.IP.address}: ${(err as Error).message}`);
2024-04-24 03:50:17 -04:00
user.protocol.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible');
}
}
onNoFlag(user: User) {
// Too late
if (user.connectedToNode) return;
user.noFlag = true;
}
2024-04-24 03:50:17 -04:00
onCapabilityUpgrade(user: User, capability: String[]): boolean {
if (user.connectedToNode) return false;
let enabledCaps = [];
for (let cap of capability) {
switch (cap) {
// binary 1.0 (msgpack rects)
case ProtocolUpgradeCapability.BinRects:
enabledCaps.push(cap as ProtocolUpgradeCapability);
user.Capabilities.bin = true;
user.protocol.dispose();
user.protocol = TheProtocolManager.createProtocol('binary1', user);
user.protocol.setHandler(this as IProtocolHandlers);
break;
default:
break;
}
}
user.protocol.sendCapabilities(enabledCaps);
return true;
}
onTurnRequest(user: User, forfeit: boolean): void {
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return;
if (!this.authCheck(user, this.Config.auth.guestPermissions.turn)) return;
if (!user.TurnRateLimit.request()) return;
if (!user.connectedToNode) return;
if (forfeit == false) {
var currentQueue = this.TurnQueue.toArray();
// If the user is already in the turn queue, ignore the turn request.
if (currentQueue.indexOf(user) !== -1) return;
// If they're muted, also ignore the turn request.
// Send them the turn queue to prevent client glitches
if (user.IP.muted) return;
if (this.Config.collabvm.turnlimit.enabled) {
// Get the amount of users in the turn queue with the same IP as the user requesting a turn.
let turns = currentQueue.filter((otheruser) => otheruser.IP.address == user.IP.address);
// If it exceeds the limit set in the config, ignore the turn request.
if (turns.length + 1 > this.Config.collabvm.turnlimit.maximum) return;
}
this.TurnQueue.enqueue(user);
if (this.TurnQueue.size === 1) this.nextTurn();
} else {
// Not sure why this wasn't using this before
this.endTurn(user);
}
this.sendTurnUpdate();
}
onVote(user: User, choice: number): void {
if (!this.VM.SnapshotsSupported()) return;
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return;
if (!user.connectedToNode) return;
if (!user.VoteRateLimit.request()) return;
switch (choice) {
case 1:
if (!this.voteInProgress) {
if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return;
if (this.voteCooldown !== 0) {
2024-08-21 22:36:22 -04:00
user.protocol.sendVoteCooldown(this.voteCooldown);
return;
}
2024-08-21 22:36:22 -04:00
this.startVote();
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has started a vote to reset the VM.`));
2024-06-25 19:56:28 -04:00
}
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== true) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted yes.`));
}
user.IP.vote = true;
break;
case 0:
if (!this.voteInProgress) return;
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== false) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted no.`));
}
user.IP.vote = false;
break;
default:
break;
}
this.sendVoteUpdate();
}
async onList(user: User) {
let listEntry: ListEntry = {
id: this.Config.collabvm.node,
name: this.Config.collabvm.displayname,
thumbnail: this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()
};
if (this.VM.GetState() == VMState.Started) {
user.protocol.sendListResponse([listEntry]);
}
}
private async connectViewShared(user: User, node: string, viewMode: number | undefined) {
if (!user.username || node !== this.Config.collabvm.node) {
user.protocol.sendConnectFailResponse();
return;
}
user.connectedToNode = true;
if (viewMode !== undefined) {
if (viewMode !== 0 && viewMode !== 1) {
user.protocol.sendConnectFailResponse();
return;
}
user.viewMode = viewMode;
}
user.protocol.sendConnectOKResponse(this.VM.SnapshotsSupported());
if (this.ChatHistory.size !== 0) {
let history = this.ChatHistory.toArray() as ChatHistory[];
user.protocol.sendChatHistoryMessage(history);
}
if (this.Config.collabvm.motd) user.protocol.sendChatMessage('', this.Config.collabvm.motd);
if (this.screenHidden) {
user?.protocol.sendScreenResize(1024, 768);
user?.protocol.sendScreenUpdate({
x: 0,
y: 0,
data: this.screenHiddenImg
});
} else {
await this.SendFullScreenWithSize(user);
}
user.protocol.sendSync(Date.now());
if (this.voteInProgress) this.sendVoteUpdate(user);
this.sendTurnUpdate(user);
}
async onConnect(user: User, node: string) {
return this.connectViewShared(user, node, undefined);
}
async onView(user: User, node: string, viewMode: number) {
return this.connectViewShared(user, node, viewMode);
}
onRename(user: User, newName: string | undefined): void {
if (!user.RenameRateLimit.request()) return;
if (user.connectedToNode && user.IP.muted) return;
if (this.Config.auth.enabled && user.rank !== Rank.Unregistered) {
user.protocol.sendChatMessage('', 'Go to your account settings to change your username.');
return;
}
if (this.Config.auth.enabled && newName !== undefined) {
// Don't send system message to a user without a username since it was likely an automated attempt by the webapp
if (user.username) user.protocol.sendChatMessage('', 'You need to log in to do that.');
if (user.rank !== Rank.Unregistered) return;
this.renameUser(user, undefined);
return;
}
this.renameUser(user, newName!);
}
onChat(user: User, message: string): void {
if (!user.username) return;
if (user.IP.muted) return;
if (!this.authCheck(user, this.Config.auth.guestPermissions.chat)) return;
var msg = Utilities.HTMLSanitize(message);
// One of the things I hated most about the old server is it completely discarded your message if it was too long
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
if (msg.trim().length < 1) return;
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, msg));
this.ChatHistory.push({ user: user.username, msg: msg });
user.onChatMsgSent();
}
onKey(user: User, keysym: number, pressed: boolean): void {
if (this.TurnQueue.peek() !== user && user.rank !== Rank.Admin) return;
this.VM.GetDisplay()?.KeyboardEvent(keysym, pressed);
}
onMouse(user: User, x: number, y: number, buttonMask: number): void {
if (this.TurnQueue.peek() !== user && user.rank !== Rank.Admin) return;
this.VM.GetDisplay()?.MouseEvent(x, y, buttonMask);
}
// TODO: make senders for admin things
async onAdminLogin(user: User, password: string) {
if (!user.LoginRateLimit.request() || !user.username) return;
var sha256 = createHash('sha256');
sha256.update(password, 'utf-8');
var pwdHash = sha256.digest('hex');
sha256.destroy();
if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
user.turnWhitelist = true;
user.protocol.sendChatMessage('', 'You may now take turns.');
return;
}
if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.');
return;
}
if (pwdHash === this.Config.collabvm.adminpass) {
user.rank = Rank.Admin;
user.protocol.sendAdminLoginResponse(true, undefined);
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
user.rank = Rank.Moderator;
user.protocol.sendAdminLoginResponse(true, this.ModPerms);
} else {
user.protocol.sendAdminLoginResponse(false, undefined);
return;
}
if (this.screenHidden) {
await this.SendFullScreenWithSize(user);
}
// Update rank
this.clients.forEach((c) =>
c.protocol.sendAddUser([
{
username: user.username!,
rank: user.rank
}
])
);
}
async onAdminMonitor(user: User, node: string, command: string) {
if (user.rank !== Rank.Admin) return;
if (node !== this.Config.collabvm.node) return;
let output = await this.VM.MonitorCommand(command);
user.protocol.sendAdminMonitorResponse(String(output));
}
onAdminRestore(user: User, node: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return;
this.VM.Reset();
}
async onAdminReboot(user: User, node: string) {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.reboot)) return;
if (node !== this.Config.collabvm.node) return;
await this.VM.Reboot();
}
onAdminBanUser(user: User, username: string): void {
// Ban
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return;
let otherUser = this.clients.find((c) => c.username === username);
if (!otherUser) return;
this.logger.info(`Banning ${otherUser.username!} (${otherUser.IP.address}) by request of ${otherUser.username!}`);
user.ban(this.banmgr);
}
onAdminForceVote(user: User, choice: number): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.forcevote)) return;
if (!this.voteInProgress) return;
this.endVote(choice == 1);
}
onAdminMuteUser(user: User, username: string, temporary: boolean): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.mute)) return;
let target = this.clients.find((c) => c.username === username);
if (!target) return;
target.mute(!temporary);
}
onAdminKickUser(user: User, username: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.kick)) return;
var target = this.clients.find((c) => c.username === username);
if (!target) return;
target.kick();
}
onAdminEndTurn(user: User, username: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return;
var target = this.clients.find((c) => c.username === username);
if (!target) return;
this.endTurn(target);
}
onAdminClearQueue(user: User, node: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return;
if (node !== this.Config.collabvm.node) return;
this.clearTurns();
}
onAdminRename(user: User, target: string, newName: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'Cannot rename users on a server that uses authentication.');
}
var targetUser = this.clients.find((c) => c.username === target);
if (!targetUser) return;
this.renameUser(targetUser, newName);
}
onAdminGetIP(user: User, username: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return;
let target = this.clients.find((c) => c.username === username);
if (!target) return;
user.protocol.sendAdminIPResponse(username, target.IP.address);
}
onAdminBypassTurn(user: User): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return;
this.bypassTurn(user);
}
onAdminRawMessage(user: User, message: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return;
switch (user.rank) {
case Rank.Admin:
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, message));
this.ChatHistory.push({ user: user.username!, msg: message });
break;
case Rank.Moderator:
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, message));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, Utilities.HTMLSanitize(message)));
break;
2024-04-24 03:50:17 -04:00
}
}
onAdminToggleTurns(user: User, enabled: boolean): void {
if (user.rank !== Rank.Admin) return;
if (enabled) {
this.turnsAllowed = true;
} else {
this.turnsAllowed = false;
this.clearTurns();
}
}
onAdminIndefiniteTurn(user: User): void {
if (user.rank !== Rank.Admin) return;
this.indefiniteTurn = user;
this.TurnQueue = Queue.from([user, ...this.TurnQueue.toArray().filter((c) => c !== user)]);
this.sendTurnUpdate();
}
async onAdminHideScreen(user: User, show: boolean) {
if (user.rank !== Rank.Admin) return;
if (show) {
// if(!this.screenHidden) return; ?
this.screenHidden = false;
let displaySize = this.VM.GetDisplay()?.Size();
if(displaySize == undefined)
return;
let encoded = await this.MakeRectData({
x: 0,
y: 0,
width: displaySize.width,
height: displaySize.height
});
this.clients.forEach(async (client) => this.SendFullScreenWithSize(client));
} else {
this.screenHidden = true;
this.clients
.filter((c) => c.rank == Rank.Unregistered)
.forEach((client) => {
client.protocol.sendScreenResize(1024, 768);
client.protocol.sendScreenUpdate({
x: 0,
y: 0,
data: this.screenHiddenImg
});
});
}
}
onAdminSystemMessage(user: User, message: string): void {
if (user.rank !== Rank.Admin) return;
this.clients.forEach((c) => c.protocol.sendChatMessage('', message));
2024-04-24 03:50:17 -04:00
}
// end IProtocolHandlers
2024-04-24 03:50:17 -04:00
getUsernameList(): string[] {
var arr: string[] = [];
this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!));
return arr;
}
2024-06-23 02:23:59 -04:00
renameUser(client: User, newName?: string, announce: boolean = true) {
2024-04-24 03:50:17 -04:00
// This shouldn't need a ternary but it does for some reason
let hadName = client.username ? true : false;
let oldname: any;
2024-04-24 03:50:17 -04:00
if (hadName) oldname = client.username;
2024-04-24 03:50:17 -04:00
if (!newName) {
client.assignGuestName(this.getUsernameList());
} else {
newName = newName.trim();
if (hadName && newName === oldname) {
client.protocol.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank);
2024-04-24 03:50:17 -04:00
return;
}
let status = ProtocolRenameStatus.Ok;
2024-04-24 03:50:17 -04:00
if (this.getUsernameList().indexOf(newName) !== -1) {
client.assignGuestName(this.getUsernameList());
if (client.connectedToNode) {
status = ProtocolRenameStatus.UsernameTaken;
2024-04-24 03:50:17 -04:00
}
} else if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName) || newName.length > 20 || newName.length < 3) {
client.assignGuestName(this.getUsernameList());
status = ProtocolRenameStatus.UsernameInvalid;
2024-04-24 03:50:17 -04:00
} else if (this.Config.collabvm.usernameblacklist.indexOf(newName) !== -1) {
client.assignGuestName(this.getUsernameList());
status = ProtocolRenameStatus.UsernameNotAllowed;
2024-04-24 03:50:17 -04:00
} else client.username = newName;
client.protocol.sendSelfRename(status, client.username!, client.rank);
2024-04-24 03:50:17 -04:00
}
if (hadName) {
this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
if (announce) this.clients.forEach((c) => c.protocol.sendRename(oldname, client.username!, client.rank));
2024-04-24 03:50:17 -04:00
} else {
this.logger.info(`Rename ${client.IP.address} to ${client.username}`);
if (announce)
this.clients.forEach((c) => {
c.protocol.sendAddUser([
{
username: client.username!,
rank: client.rank
}
]);
if (client.countryCode !== null) {
c.protocol.sendFlag([
{
username: client.username!,
countryCode: client.countryCode
}
]);
}
});
2024-04-24 03:50:17 -04:00
}
}
private getAddUser(): ProtocolAddUser[] {
return this.clients
.filter((c) => c.username)
.map((c) => {
return {
username: c.username!,
rank: c.rank
};
});
2024-04-24 03:50:17 -04:00
}
private getFlags(): ProtocolFlag[] {
let arr = [];
for (let c of this.clients.filter((cl) => cl.countryCode !== null && cl.username && (!cl.noFlag || cl.rank === Rank.Unregistered))) {
arr.push({
username: c.username!,
countryCode: c.countryCode!
});
2024-06-23 02:23:59 -04:00
}
return arr;
2024-06-23 02:23:59 -04:00
}
2024-04-24 03:50:17 -04:00
private sendTurnUpdate(client?: User) {
var turnQueueArr = this.TurnQueue.toArray();
var turntime: number;
2024-04-24 03:50:17 -04:00
if (this.indefiniteTurn === null) turntime = this.TurnTime * 1000;
else turntime = 9999999999;
var users: string[] = [];
this.TurnQueue.forEach((c) => users.push(c.username!));
2024-04-24 03:50:17 -04:00
var currentTurningUser = this.TurnQueue.peek();
2024-04-24 03:50:17 -04:00
if (client) {
client.protocol.sendTurnQueue(turntime, users);
2024-04-24 03:50:17 -04:00
return;
}
2024-04-24 03:50:17 -04:00
this.clients
.filter((c) => c !== currentTurningUser && c.connectedToNode)
.forEach((c) => {
if (turnQueueArr.indexOf(c) !== -1) {
var time;
if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000;
else time = 9999999999;
c.protocol.sendTurnQueueWaiting(turntime, users, time);
2024-04-24 03:50:17 -04:00
} else {
c.protocol.sendTurnQueue(turntime, users);
2024-04-24 03:50:17 -04:00
}
});
if (currentTurningUser) currentTurningUser.protocol.sendTurnQueue(turntime, users);
2024-04-24 03:50:17 -04:00
}
private nextTurn() {
clearInterval(this.TurnInterval);
if (this.TurnQueue.size === 0) {
} else {
this.TurnTime = this.Config.collabvm.turnTime;
this.TurnInterval = setInterval(() => this.turnInterval(), 1000);
}
this.sendTurnUpdate();
}
clearTurns() {
clearInterval(this.TurnInterval);
this.TurnQueue.clear();
this.sendTurnUpdate();
}
bypassTurn(client: User) {
var a = this.TurnQueue.toArray().filter((c) => c !== client);
this.TurnQueue = Queue.from([client, ...a]);
this.nextTurn();
}
endTurn(client: User) {
// I must have somehow accidentally removed this while scalpaling everything out
if (this.indefiniteTurn === client) this.indefiniteTurn = null;
2024-04-24 03:50:17 -04:00
var hasTurn = this.TurnQueue.peek() === client;
this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((c) => c !== client));
if (hasTurn) this.nextTurn();
else this.sendTurnUpdate();
}
private turnInterval() {
if (this.indefiniteTurn !== null) return;
this.TurnTime--;
if (this.TurnTime < 1) {
this.TurnQueue.dequeue();
this.nextTurn();
}
}
private OnDisplayRectangle(rect: Rect) {
this.rectQueue.push(rect);
2024-04-24 03:50:17 -04:00
}
private OnDisplayResized(size: Size) {
this.clients
.filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenResize(size.width, size.height);
2024-04-24 03:50:17 -04:00
});
}
private async OnDisplayFrame() {
let self = this;
let doRect = async (rect: Rect) => {
let encoded = await this.MakeRectData(rect);
self.clients
.filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => {
if (self.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenUpdate({
x: rect.x,
y: rect.y,
data: encoded
});
});
};
let promises: Promise<void>[] = [];
for (let rect of self.rectQueue) promises.push(doRect(rect));
this.rectQueue = [];
await Promise.all(promises);
}
2024-04-24 03:50:17 -04:00
private async SendFullScreenWithSize(client: User) {
let display = this.VM.GetDisplay();
2024-07-11 20:49:49 -04:00
if (display == null) return;
2024-04-24 03:50:17 -04:00
let displaySize = display.Size();
let encoded = await this.MakeRectData({
x: 0,
y: 0,
width: displaySize.width,
height: displaySize.height
});
client.protocol.sendScreenResize(displaySize.width, displaySize.height);
2024-06-25 19:56:28 -04:00
client.protocol.sendScreenUpdate({
x: 0,
y: 0,
data: encoded
});
2024-04-24 03:50:17 -04:00
}
private async MakeRectData(rect: Rect) {
let display = this.VM.GetDisplay();
2024-06-23 02:55:05 -04:00
// TODO: actually throw an error here
2024-07-11 20:49:49 -04:00
if (display == null) return Buffer.from('no');
2024-06-23 02:55:05 -04:00
2024-07-11 20:49:49 -04:00
let displaySize = display.Size();
let encoded = await JPEGEncoder.Encode(display.Buffer(), displaySize, rect);
2024-04-24 03:50:17 -04:00
2024-06-25 19:56:28 -04:00
return encoded;
2024-04-24 03:50:17 -04:00
}
async getThumbnail(): Promise<Buffer> {
let display = this.VM.GetDisplay();
// oh well
if (!display?.Connected()) return Buffer.alloc(4);
2024-04-24 03:50:17 -04:00
return JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size());
2024-04-24 03:50:17 -04:00
}
startVote() {
if (this.voteInProgress) return;
this.voteInProgress = true;
2024-08-21 22:36:22 -04:00
this.clients.forEach((c) => c.protocol.sendVoteStarted());
2024-04-24 03:50:17 -04:00
this.voteTime = this.Config.collabvm.voteTime;
this.voteInterval = setInterval(() => {
this.voteTime--;
if (this.voteTime < 1) {
this.endVote();
}
}, 1000);
}
endVote(result?: boolean) {
if (!this.voteInProgress) return;
this.voteInProgress = false;
clearInterval(this.voteInterval);
var count = this.getVoteCounts();
2024-08-21 22:36:22 -04:00
this.clients.forEach((c) => c.protocol.sendVoteEnded());
2024-04-24 03:50:17 -04:00
if (result === true || (result === undefined && count.yes >= count.no)) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has won.'));
2024-04-24 03:50:17 -04:00
this.VM.Reset();
} else {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has lost.'));
2024-04-24 03:50:17 -04:00
}
this.clients.forEach((c) => {
c.IP.vote = null;
});
this.voteCooldown = this.Config.collabvm.voteCooldown;
this.voteCooldownInterval = setInterval(() => {
this.voteCooldown--;
if (this.voteCooldown < 1) clearInterval(this.voteCooldownInterval);
}, 1000);
}
sendVoteUpdate(client?: User) {
if (!this.voteInProgress) return;
var count = this.getVoteCounts();
2024-08-21 22:36:22 -04:00
if (client) client.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no);
else this.clients.forEach((c) => c.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no));
2024-04-24 03:50:17 -04:00
}
getVoteCounts(): VoteTally {
let yes = 0;
let no = 0;
IPDataManager.ForEachIPData((c) => {
if (c.vote === true) yes++;
if (c.vote === false) no++;
});
return { yes: yes, no: no };
}
}