multiplayerpiano/src/Client.ts

475 lines
14 KiB
TypeScript

/**
* Multiplayer Piano Server
* Copyright (c) The Dev Channel 2020-2022
* Licensed under the GPL v3.0 license
*
* Server-side client module
*/
import { IncomingMessage } from 'http';
import { EventEmitter } from 'stream';
import * as WebSocket from 'ws';
import { Channel, ChannelSettings } from './Channel';
import { Crypto } from './Crypto';
import { Database, User } from './Database';
import { PublicUser } from './models/User';
import { RateLimit, RateLimitChain } from './RateLimit';
import { Server } from './Server';
class Client extends EventEmitter {
server: Server;
ws: WebSocket;
participantID: string;
user: User;
currentChannelID: string;
rateLimits: ClientRateLimits;
cursor: Cursor;
idleTimeout: any;
subscribedToChannelList: boolean;
constructor (server: Server, ws: WebSocket, req: IncomingMessage, id: string) {
super();
this.server = server;
this.participantID = id;
let _id = Crypto.getUser_ID(req.socket.remoteAddress.substring('::ffff:'.length));
let user: any = Database.getDefaultUser();
user._id = _id;
this.user = user;
this.cursor = new Cursor(200, -200);
this.rateLimits = new ClientRateLimits();
this.subscribedToChannelList = false;
Database.getUser(_id).then(val => {
this.user = (val as User);
this.ws = ws;
this.bindEventListeners();
});
}
bindEventListeners() { // TODO all event listeners
this.ws.on('message', (data, isBinary) => {
let d: any = data;
if (isBinary) {
d = data.toString();
}
try {
let msgs = JSON.parse(d);
if (typeof msgs !== 'object') return;
for (let msg of msgs) {
this.emit(msg.m, msg);
}
} catch (err) {
}
});
this.ws.on('close', () => { //* finshed
this.emit('bye');
this.server.destroyClient(this);
});
this.once('hi', msg => { //* finished
this.sendHiMessage();
this.restartIdleTimeout();
});
this.on('bye', msg => { //* finished
let ch = this.getChannel();
if (ch) {
ch.removeClient(this);
// ch.emit('update');
}
});
this.on('ch', msg => {
if (this.ws.readyState !== WebSocket.OPEN) return;
// console.log('---ch debug---');
// console.log(msg);
if (!msg._id) return;
// console.log('has _id')
if (typeof msg._id !== "string") return;
if (msg._id == this.currentChannelID) return;
// console.log('_id is string');
let set: ChannelSettings = Database.getDefaultChannelSettings();
// console.log('got default settings');
if (msg.set) set = msg.set;
// console.log("set: ");
// console.log(set);
this.setChannel(msg._id, set);
});
this.on('n', (msg, admin) => { // TODO n
// {
// m: 'n',
// t: 128429035891,
// n: [
// {
// n: "c3",
// v: 0.75
// },
// {
// n: 'c3',
// d: 100,
// s: 1
// }
// ]
// }
if (msg.t == null) msg.t = Date.now();
// console.log("note: ", msg);
// check properties
if (!msg.n) return;
// if (!msg.t) return;
// check types
// if (typeof msg.t !== 'number') return;
// if ((msg.t && typeof msg.t !== 'number') || msg.t == null) msg.t = Date.now();
if (!Array.isArray(msg.n)) return;
let ch: Channel = this.getChannel();
let p = this.getOwnParticipant();
if (!ch && p._id) return;
// if (!admin) {
// if (!this.rateLimits.nq.attempt(msg.t)) return;
// }
if (ch.settings.crownsolo == true) {
if (ch.crown.userId == p._id) {
// console.log(msg);
ch.sendNoteMessage(p, msg);
}
} else {
// console.log(msg);
ch.sendNoteMessage(p, msg);
}
});
this.on('m', (msg, admin) => { // TODO m
if (!this.rateLimits.m.attempt()) return;
this.setCursorPosition(msg.x, msg.y);
});
this.on('t', msg => { //* finished
this.restartIdleTimeout();
this.sendTimeMessage(msg);
});
this.on('a', msg => { // TODO chat
if (!msg.t) msg.t = Date.now();
if (!msg.message) return;
if (typeof msg.message !== 'string') return;
if (!this.rateLimits.a.attempt()) return;
let ch = this.server.channels.get(this.currentChannelID);
ch.sendChat(this.getOwnParticipant(), msg);
});
this.on('userset', (msg, admin) => {
if (!msg.set) return;
if (!msg.set.name && !msg.set.color) return;
if (typeof msg.set.name !== 'string') return;
if (msg.color && typeof msg.color !== 'string') return;
if (msg.set.name.length > 40) return;
let colorEnabled = false;
let isAdmin = admin;
if (colorEnabled && msg.set.color) {
// check color regex
if (!/^#[0-9a-f]{6}$/i.test(msg.set.color)) return;
}
this.userset({name: msg.set.name, color: msg.set.color}, isAdmin);
});
this.on('chset', (msg, admin) => {
if (!msg.set) return;
let ch = this.getChannel();
if (!admin && (ch.crown?.userId !== this.getOwnParticipant()._id)) return;
ch.setSettings(msg.set, admin);
});
this.on('chown', (msg, admin) => {
if (msg.id && typeof msg.id !== 'string') delete msg.id;
let ch = this.getChannel();
ch.setCrown(this.getOwnParticipant(), msg.id, admin);
});
this.on('+ls', (msg, admin) => { // TODO +ls
// this.subscribeToChannelList();
this.subscribedToChannelList = true;
// console.log('subsribe to channel list');
let chinfos = this.server.getChannelInfos();
this.sendChannelListUpdate(true, chinfos);
});
this.on('-ls', (msg, admin) => { // TODO -ls
// this.unsubscribeFromChannelList();
this.subscribedToChannelList = false;
// console.log('unsubscribe from channel list');
});
this.on('admin message', msg => { // TODO admin message
if (!msg.msg) return;
if (!msg.password) return;
if (msg.password !== Database.adminPassword) return;
if (typeof msg.msg !== 'object') return;
this.emit(msg.msg.m, msg.msg, true);
});
this.on('subscribe to admin stream', (msg, admin) => { // TODO subscribe to admin stream
if (!admin) return;
});
this.on('unsubscribe from admin stream', (msg, admin) => { // TODO unsubscribe from admin stream
if (!admin) return;
});
this.on('user_flag', (msg, admin) => { // TODO user_flag
if (!admin) return;
});
this.on('color', (msg, admin) => {
if (!admin) return;
if (!msg.color) return;
if (typeof msg.color !== 'string') return;
if (!/^#[0-9a-f]{6}$/i.test(msg.color)) return;
let cl = msg._id ? this.server.findClientBy_ID(msg._id) : this;
cl.userset({color: msg.color}, admin);
});
this.on('debug', (msg, admin) => {
console.log(this);
});
}
getOwnParticipant(): PublicUser { //* finished
let u = this.user;
// remember to 'clean' the user object
delete u.flags;
u.id = this.participantID;
return u;
}
getChannel(): Channel {
return this.server.channels.get(this.currentChannelID);
}
sendHiMessage(): void { //* finished
this.sendArray([{
m: 'hi',
motd: "galvanized saga",
u: this.getOwnParticipant(),
v: '3.0',
t: Date.now()
}]);
}
sendTimeMessage(msg?: any): void { //* finished
this.sendArray([{
m: 't',
t: Date.now(),
e: msg ? msg.t ? msg.t : undefined : undefined
}]);
}
restartIdleTimeout() {
// console.log('restarting idle timeout for ' + this.participantID);
// clearTimeout(this.idleTimeout);
// this.idleTimeout = setTimeout(() => {
// // console.log('idle timeout reached for ' + this.participantID);
// this.emit('bye');
// }, 30000);
}
sendArray(msgarr: any[]) { //* finished
let json = JSON.stringify(msgarr);
this.send(json);
}
send(json: string) { //* finished
try {
this.ws.send(json);
} catch (err) {
}
}
sendChannelListUpdate(complete, chinfos) {
this.sendArray([{
m: 'ls',
c: complete,
u: chinfos
}]);
}
setCursorPosition(x: number, y: number) {
if (typeof x !== 'number' || typeof y !== 'number') {
if (typeof x == 'string') {
x = parseInt(x);
if (isNaN(x)) return;
} else {
return;
}
if (typeof y == 'string') {
y = parseInt(y);
if (isNaN(y)) return;
} else {
return;
}
}
this.cursor.x = x;
this.cursor.y = y;
let ch = this.server.channels.get(this.currentChannelID);
if (ch) {
ch.sendCursorPosition(this.getOwnParticipant(), x, y);
}
}
sendChatHistory(c: any[]) {
this.sendArray([{
m: 'c',
c: c
}]);
}
async userset(set: any, admin: boolean = false, _id?: string) {
let _idToGet = this.getOwnParticipant()._id;
if (admin && _id) _idToGet = _id;
let user = await Database.getUser(_idToGet);
if (!user) return;
if (set.name) user.name = set.name;
if (set.color && admin) user.color = set.color;
await Database.updateUser(_idToGet, user);
this.user.name = user.name;
this.user.color = user.color;
let ch = this.server.channels.get(this.currentChannelID);
if (ch) {
ch.sendUserUpdate(this.getOwnParticipant(), this.cursor.x, this.cursor.y);
}
}
setChannel(_id: string, set?: ChannelSettings) { // TODO setChannel
// console.log('set channel called', this.server.channels);
// check if server has channel
if (!this.server.channels.get(_id)) {
//console.log('channel does not exist, creating new channel');
let ch = new Channel(this.server, _id, set, this.getOwnParticipant(), 50, this.cursor.y);
//console.log('channel debug'):;
//console.log(ch);
if (this.currentChannelID == ch._id) this.emit("bye");
ch.addClient(this);
this.server.channels.set(_id, ch);
return;
}
// console.log('channel exists');
this.emit("bye");
this.server.channels.get(_id).addClient(this);
}
sendChannelMessage(ch: Channel) {
// console.log('sending channel message');
let ppl = ch.getParticipantList();
// console.log('ppl: ', ppl);
let msg = {
m: 'ch',
ch: {
settings: ch.settings,
_id: ch._id,
count: ch.connectedClients.length,
crown: ch.crown
},
ppl: ppl,
p: this.participantID
}
// console.log(msg);
this.sendArray([msg]);
}
// subscribeToChannelList() { // TODO channel listing and subscribing
// this.server.setChannelListSubscriber(this.getOwnParticipant()._id);
// }
// unsubscribeFromChannelList() { // TODO channel listing and subscribing
// this.server.unsetChannelListSubscriber(this.getOwnParticipant()._id);
// }
sendParticipantMessage(p, cursor) {
let msg = {
m: 'p',
_id: p._id,
name: p.name,
color: p.color,
id: p.id,
x: cursor.x,
y: cursor.y
}
this.sendArray([msg]);
}
sendData(data) { // TODO admin data
data.m = 'data';
this.sendArray([data]);
}
}
class ClientRateLimits {
m: RateLimit;
ch: RateLimit;
chset: RateLimit;
nq: RateLimitChain;
t: RateLimit;
a: RateLimitChain;
constructor () {
let data = Database.getDefaultClientRateLimits();
for (let key of Object.keys(data)) {
this[key] = data[key];
}
// console.log('rate limit debug -----');
// console.log(this);
}
}
class Cursor {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
interface Userset {
name?: string;
color?: string;
}
export {
Client
}