2024-04-23 09:57:02 -04:00
|
|
|
import * as Utilities from './Utilities.js';
|
2024-06-22 21:14:05 -04:00
|
|
|
import * as cvm from '@cvmts/cvm-rs';
|
2024-04-23 09:57:02 -04:00
|
|
|
import { IPData } from './IPData.js';
|
|
|
|
|
import IConfig from './IConfig.js';
|
|
|
|
|
import RateLimiter from './RateLimiter.js';
|
|
|
|
|
import { execa, execaCommand, ExecaSyncError } from 'execa';
|
|
|
|
|
import { Logger } from '@cvmts/shared';
|
2024-05-26 23:19:55 -04:00
|
|
|
import NetworkClient from './NetworkClient.js';
|
2024-06-25 21:24:08 -04:00
|
|
|
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
export class User {
|
2024-05-26 23:19:55 -04:00
|
|
|
socket: NetworkClient;
|
2024-04-23 09:57:02 -04:00
|
|
|
nopSendInterval: NodeJS.Timeout;
|
|
|
|
|
msgRecieveInterval: NodeJS.Timeout;
|
|
|
|
|
nopRecieveTimeout?: NodeJS.Timeout;
|
|
|
|
|
username?: string;
|
|
|
|
|
connectedToNode: boolean;
|
|
|
|
|
viewMode: number;
|
|
|
|
|
rank: Rank;
|
|
|
|
|
msgsSent: number;
|
|
|
|
|
Config: IConfig;
|
|
|
|
|
IP: IPData;
|
2024-06-25 19:56:28 -04:00
|
|
|
Capabilities: CollabVMCapabilities;
|
2024-06-23 02:23:59 -04:00
|
|
|
// Hide flag. Only takes effect if the user is logged in.
|
|
|
|
|
noFlag: boolean = false;
|
|
|
|
|
countryCode: string | null = null;
|
2024-04-23 09:57:02 -04:00
|
|
|
// Rate limiters
|
|
|
|
|
ChatRateLimit: RateLimiter;
|
|
|
|
|
LoginRateLimit: RateLimiter;
|
|
|
|
|
RenameRateLimit: RateLimiter;
|
|
|
|
|
TurnRateLimit: RateLimiter;
|
|
|
|
|
VoteRateLimit: RateLimiter;
|
|
|
|
|
|
2024-04-24 03:50:17 -04:00
|
|
|
private logger = new Logger('CVMTS.User');
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2024-05-26 23:19:55 -04:00
|
|
|
constructor(socket: NetworkClient, ip: IPData, config: IConfig, username?: string, node?: string) {
|
2024-04-23 09:57:02 -04:00
|
|
|
this.IP = ip;
|
|
|
|
|
this.connectedToNode = false;
|
|
|
|
|
this.viewMode = -1;
|
|
|
|
|
this.Config = config;
|
2024-05-26 23:19:55 -04:00
|
|
|
this.socket = socket;
|
2024-04-23 09:57:02 -04:00
|
|
|
this.msgsSent = 0;
|
2024-06-25 19:56:28 -04:00
|
|
|
this.Capabilities = new CollabVMCapabilities();
|
2024-06-19 18:26:27 -04:00
|
|
|
|
2024-05-26 23:19:55 -04:00
|
|
|
this.socket.on('disconnect', () => {
|
2024-06-19 18:26:27 -04:00
|
|
|
// Unref the ip data for this connection
|
|
|
|
|
this.IP.Unref();
|
|
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
clearInterval(this.nopSendInterval);
|
2024-05-26 23:19:55 -04:00
|
|
|
clearInterval(this.msgRecieveInterval);
|
2024-04-23 09:57:02 -04:00
|
|
|
});
|
2024-06-19 18:26:27 -04:00
|
|
|
|
2024-05-26 23:19:55 -04:00
|
|
|
this.socket.on('msg', (e) => {
|
2024-04-23 09:57:02 -04:00
|
|
|
clearTimeout(this.nopRecieveTimeout);
|
|
|
|
|
clearInterval(this.msgRecieveInterval);
|
|
|
|
|
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
|
|
|
|
|
});
|
2024-06-22 21:26:49 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
this.nopSendInterval = setInterval(() => this.sendNop(), 5000);
|
|
|
|
|
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
|
|
|
|
|
this.sendNop();
|
|
|
|
|
if (username) this.username = username;
|
|
|
|
|
this.rank = 0;
|
|
|
|
|
this.ChatRateLimit = new RateLimiter(this.Config.collabvm.automute.messages, this.Config.collabvm.automute.seconds);
|
|
|
|
|
this.ChatRateLimit.on('limit', () => this.mute(false));
|
|
|
|
|
this.RenameRateLimit = new RateLimiter(3, 60);
|
|
|
|
|
this.RenameRateLimit.on('limit', () => this.closeConnection());
|
|
|
|
|
this.LoginRateLimit = new RateLimiter(4, 3);
|
|
|
|
|
this.LoginRateLimit.on('limit', () => this.closeConnection());
|
|
|
|
|
this.TurnRateLimit = new RateLimiter(5, 3);
|
|
|
|
|
this.TurnRateLimit.on('limit', () => this.closeConnection());
|
|
|
|
|
this.VoteRateLimit = new RateLimiter(3, 3);
|
|
|
|
|
this.VoteRateLimit.on('limit', () => this.closeConnection());
|
|
|
|
|
}
|
2024-06-22 21:26:49 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
assignGuestName(existingUsers: string[]): string {
|
|
|
|
|
var username;
|
|
|
|
|
do {
|
|
|
|
|
username = 'guest' + Utilities.Randint(10000, 99999);
|
|
|
|
|
} while (existingUsers.indexOf(username) !== -1);
|
|
|
|
|
this.username = username;
|
|
|
|
|
return username;
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
sendNop() {
|
|
|
|
|
this.socket.send('3.nop;');
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-05-26 23:19:55 -04:00
|
|
|
sendMsg(msg: string) {
|
|
|
|
|
if (!this.socket.isOpen()) return;
|
2024-04-23 09:57:02 -04:00
|
|
|
clearInterval(this.nopSendInterval);
|
|
|
|
|
this.nopSendInterval = setInterval(() => this.sendNop(), 5000);
|
2024-06-19 18:20:41 -04:00
|
|
|
this.socket.send(msg);
|
2024-04-23 09:57:02 -04:00
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
private onNoMsg() {
|
|
|
|
|
this.sendNop();
|
|
|
|
|
this.nopRecieveTimeout = setTimeout(() => {
|
|
|
|
|
this.closeConnection();
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
closeConnection() {
|
2024-06-22 21:14:05 -04:00
|
|
|
this.socket.send(cvm.guacEncode('disconnect'));
|
2024-04-23 09:57:02 -04:00
|
|
|
this.socket.close();
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
onMsgSent() {
|
|
|
|
|
if (!this.Config.collabvm.automute.enabled) return;
|
|
|
|
|
// rate limit guest and unregistered chat messages, but not staff ones
|
|
|
|
|
switch (this.rank) {
|
|
|
|
|
case Rank.Moderator:
|
|
|
|
|
case Rank.Admin:
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
this.ChatRateLimit.request();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
mute(permanent: boolean) {
|
|
|
|
|
this.IP.muted = true;
|
2024-06-22 21:14:05 -04:00
|
|
|
this.sendMsg(cvm.guacEncode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
|
2024-04-23 09:57:02 -04:00
|
|
|
if (!permanent) {
|
|
|
|
|
clearTimeout(this.IP.tempMuteExpireTimeout);
|
|
|
|
|
this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
unmute() {
|
|
|
|
|
clearTimeout(this.IP.tempMuteExpireTimeout);
|
|
|
|
|
this.IP.muted = false;
|
2024-06-22 21:14:05 -04:00
|
|
|
this.sendMsg(cvm.guacEncode('chat', '', 'You are no longer muted.'));
|
2024-04-23 09:57:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private banCmdArgs(arg: string): string {
|
|
|
|
|
return arg.replace(/\$IP/g, this.IP.address).replace(/\$NAME/g, this.username || '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async ban() {
|
|
|
|
|
// Prevent the user from taking turns or chatting, in case the ban command takes a while
|
|
|
|
|
this.IP.muted = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (Array.isArray(this.Config.collabvm.bancmd)) {
|
|
|
|
|
let args: string[] = this.Config.collabvm.bancmd.map((a: string) => this.banCmdArgs(a));
|
|
|
|
|
if (args.length || args[0].length) {
|
|
|
|
|
await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr });
|
|
|
|
|
this.kick();
|
|
|
|
|
} else {
|
|
|
|
|
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`);
|
|
|
|
|
}
|
|
|
|
|
} else if (typeof this.Config.collabvm.bancmd == 'string') {
|
|
|
|
|
let cmd: string = this.banCmdArgs(this.Config.collabvm.bancmd);
|
|
|
|
|
if (cmd.length) {
|
|
|
|
|
await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr });
|
|
|
|
|
this.kick();
|
|
|
|
|
} else {
|
|
|
|
|
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): ${(e as ExecaSyncError).shortMessage}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async kick() {
|
|
|
|
|
this.sendMsg('10.disconnect;');
|
|
|
|
|
this.socket.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export enum Rank {
|
|
|
|
|
Unregistered = 0,
|
|
|
|
|
// After all these years
|
|
|
|
|
Registered = 1,
|
|
|
|
|
Admin = 2,
|
|
|
|
|
Moderator = 3,
|
|
|
|
|
// Giving a good gap between server only internal ranks just in case
|
|
|
|
|
Turn = 10
|
|
|
|
|
}
|