354 lines
10 KiB
JavaScript
354 lines
10 KiB
JavaScript
|
const Channel = require("./Channel.js");
|
||
|
const Quota = require("./Quota.js");
|
||
|
const quotas = require("../Quotas");
|
||
|
const { RateLimit, RateLimitChain } = require("./Ratelimit.js");
|
||
|
const User = require("./User.js");
|
||
|
const Database = require("./Database.js");
|
||
|
const { EventEmitter } = require("events");
|
||
|
|
||
|
class Client extends EventEmitter {
|
||
|
/**
|
||
|
* Server-side client representation
|
||
|
* @param {*} ws WebSocket object
|
||
|
* @param {*} req WebSocket request
|
||
|
* @param {*} server Server
|
||
|
*/
|
||
|
constructor(ws, req, server) {
|
||
|
super();
|
||
|
EventEmitter.call(this);
|
||
|
this.connectionid = server.connectionid;
|
||
|
this.server = server;
|
||
|
this.participantId;
|
||
|
this.channel;
|
||
|
this.isSubscribedToAdminStream = false;
|
||
|
this.adminStreamInterval;
|
||
|
|
||
|
this.staticQuotas = {
|
||
|
room: new RateLimit(quotas.room.time)
|
||
|
};
|
||
|
|
||
|
this.quotas = {};
|
||
|
this.ws = ws;
|
||
|
this.req = req;
|
||
|
this.ip = req.connection.remoteAddress.replace("::ffff:", "");
|
||
|
this.hidden = false;
|
||
|
|
||
|
Database.getUserData(this, server).then(data => {
|
||
|
this.user = new User(this, data);
|
||
|
this.destroied = false;
|
||
|
this.bindEventListeners();
|
||
|
require("./Message.js")(this);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if user is connected
|
||
|
* @returns boolean
|
||
|
*/
|
||
|
isConnected() {
|
||
|
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if user is connecting
|
||
|
* @returns boolean
|
||
|
*/
|
||
|
isConnecting() {
|
||
|
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Move user to channel
|
||
|
* @param {string} _id User ID
|
||
|
* @param {*} settings Settings object
|
||
|
* @returns undefined
|
||
|
*/
|
||
|
setChannel(_id, settings) {
|
||
|
if (this.channel && this.channel._id == _id) return;
|
||
|
if (this.server.channels.get(_id)) {
|
||
|
let ch = this.server.channels.get(_id, settings);
|
||
|
let userbanned = ch.bans.get(this.user._id);
|
||
|
|
||
|
if (
|
||
|
userbanned &&
|
||
|
Date.now() - userbanned.bannedtime >= userbanned.msbanned
|
||
|
) {
|
||
|
ch.bans.delete(userbanned.user._id);
|
||
|
userbanned = undefined;
|
||
|
}
|
||
|
|
||
|
if (userbanned) {
|
||
|
new Notification(this.server, {
|
||
|
targetUser: this.participantId,
|
||
|
targetChannel: all,
|
||
|
title: "Notice",
|
||
|
text: `Currently banned from "${_id}" for ${Math.ceil(
|
||
|
Math.floor(
|
||
|
(userbanned.msbanned -
|
||
|
(Date.now() - userbanned.bannedtime)) /
|
||
|
1000
|
||
|
) / 60
|
||
|
)} minutes.`,
|
||
|
duration: 7000,
|
||
|
id: "",
|
||
|
target: "#room",
|
||
|
class: "short",
|
||
|
cl: this
|
||
|
}).send();
|
||
|
this.setChannel(Channel.banChannel, settings);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let channel = this.channel;
|
||
|
if (channel) this.channel.emit("bye", this);
|
||
|
if (channel) this.channel.updateCh(this);
|
||
|
|
||
|
this.channel = this.server.channels.get(_id);
|
||
|
this.channel.join(this);
|
||
|
} else {
|
||
|
let room = new Channel(this.server, _id, settings, this);
|
||
|
this.server.channels.set(_id, room);
|
||
|
if (this.channel) this.channel.emit("bye", this);
|
||
|
this.channel = this.server.channels.get(_id);
|
||
|
this.channel.join(this, settings);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send data to client
|
||
|
* @param {any[]} arr Array of messages
|
||
|
*/
|
||
|
sendArray(arr) {
|
||
|
if (this.isConnected()) {
|
||
|
//console.log(`SEND: `, JSON.colorStringify(arr));
|
||
|
this.ws.send(JSON.stringify(arr));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set username in database
|
||
|
* @param {string} name Username
|
||
|
* @param {boolean} admin Is admin?
|
||
|
* @returns undefined
|
||
|
*/
|
||
|
userset(name, admin) {
|
||
|
if (name.length > 40 && !admin) return;
|
||
|
|
||
|
if (this.quotas.userset) {
|
||
|
if (!this.quotas.userset.attempt()) return;
|
||
|
}
|
||
|
|
||
|
if (!this.user.hasFlag("freeze_name", true) || admin) {
|
||
|
this.user.name = name;
|
||
|
|
||
|
if (!this.user.hasFlag("freeze_name", true)) {
|
||
|
Database.getUserData(this, this.server).then(usr => {
|
||
|
Database.updateUser(this.user._id, this.user);
|
||
|
|
||
|
this.server.channels.forEach(channel => {
|
||
|
channel.updateParticipant(this.user._id, {
|
||
|
name: name
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set rate limits
|
||
|
*/
|
||
|
initParticipantQuotas() {
|
||
|
this.quotas = {
|
||
|
//"chat": new Quota(Quota.PARAMS_A_NORMAL),
|
||
|
chat: {
|
||
|
lobby: new RateLimitChain(
|
||
|
quotas.chat.lobby.amount,
|
||
|
quotas.chat.lobby.time
|
||
|
),
|
||
|
normal: new RateLimitChain(
|
||
|
quotas.chat.normal.amount,
|
||
|
quotas.chat.normal.time
|
||
|
),
|
||
|
insane: new RateLimitChain(
|
||
|
quotas.chat.insane.amount,
|
||
|
quotas.chat.insane.time
|
||
|
)
|
||
|
},
|
||
|
cursor: new RateLimitChain(
|
||
|
quotas.cursor.amount,
|
||
|
quotas.cursor.time
|
||
|
),
|
||
|
chown: new RateLimitChain(quotas.chown.amount, quotas.chown.time),
|
||
|
userset: new RateLimitChain(
|
||
|
quotas.userset.amount,
|
||
|
quotas.userset.time
|
||
|
),
|
||
|
kickban: new RateLimitChain(
|
||
|
quotas.kickban.amount,
|
||
|
quotas.kickban.time
|
||
|
),
|
||
|
// note: new Quota(Quota.PARAMS_LOBBY),
|
||
|
note: new RateLimitChain(5, 5000),
|
||
|
chset: new Quota(Quota.PARAMS_USED_A_LOT),
|
||
|
"+ls": new Quota(Quota.PARAMS_USED_A_LOT),
|
||
|
"-ls": new Quota(Quota.PARAMS_USED_A_LOT)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stop the client
|
||
|
*/
|
||
|
destroy() {
|
||
|
if (this.user) {
|
||
|
let lastClient = true;
|
||
|
|
||
|
for (const cl of this.server.connections.values()) {
|
||
|
if (cl.user) {
|
||
|
if (cl.user._id == this.user._id) {
|
||
|
lastClient = false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (lastClient) this.user.stopFlagEvents();
|
||
|
}
|
||
|
|
||
|
this.ws.close();
|
||
|
|
||
|
if (this.channel) {
|
||
|
this.channel.emit("bye", this);
|
||
|
}
|
||
|
this.user;
|
||
|
this.participantId;
|
||
|
this.channel;
|
||
|
this.server.roomlisteners.delete(this.connectionid);
|
||
|
this.connectionid;
|
||
|
this.server.connections.delete(this.connectionid);
|
||
|
this.destroied = true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Internal
|
||
|
*/
|
||
|
bindEventListeners() {
|
||
|
this.ws.on("message", (evt, admin) => {
|
||
|
try {
|
||
|
if (typeof evt !== "string") evt = evt.toJSON();
|
||
|
let transmission = JSON.parse(evt);
|
||
|
for (let msg of transmission) {
|
||
|
if (typeof msg !== "object" || msg == null || msg == NaN)
|
||
|
return;
|
||
|
if (!msg.hasOwnProperty("m")) return;
|
||
|
if (!this.server.legit_m.includes(msg.m)) return;
|
||
|
this.emit(msg.m, msg, !!admin);
|
||
|
//console.log(`RECIEVE: `, JSON.colorStringify(msg));
|
||
|
}
|
||
|
} catch (e) {
|
||
|
console.log(e);
|
||
|
// this.destroy();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.ws.on("close", () => {
|
||
|
if (!this.destroied) {
|
||
|
this.destroy();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.ws.addEventListener("error", err => {
|
||
|
console.error(err);
|
||
|
if (!this.destroied) {
|
||
|
this.destroy();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send admin data bus message
|
||
|
*/
|
||
|
sendAdminData() {
|
||
|
let data = {};
|
||
|
data.m = "data";
|
||
|
|
||
|
let channels = [];
|
||
|
this.server.channels.forEach(ch => {
|
||
|
let ppl = [];
|
||
|
const chdata = ch.fetchChannelData();
|
||
|
|
||
|
for (let p of chdata.ppl) {
|
||
|
ppl.push({
|
||
|
user: p
|
||
|
});
|
||
|
}
|
||
|
|
||
|
channels.push({
|
||
|
chat: ch.chatmsgs.slice(-1 * 32),
|
||
|
participants: ppl
|
||
|
});
|
||
|
});
|
||
|
|
||
|
let users = [];
|
||
|
let clients = [];
|
||
|
this.server.connections.forEach(cl => {
|
||
|
if (!cl.user) return;
|
||
|
let c = {
|
||
|
ip: cl.ip,
|
||
|
participantId: cl.participantId,
|
||
|
userId: cl.user._id
|
||
|
};
|
||
|
clients.push(c);
|
||
|
let u = {
|
||
|
p: {
|
||
|
_id: cl.user._id,
|
||
|
name: cl.user.name,
|
||
|
color: cl.user.color,
|
||
|
flags: cl.user.flags
|
||
|
// inventory: cl.user.inventory
|
||
|
},
|
||
|
id: cl.participantId
|
||
|
};
|
||
|
|
||
|
users.push(u);
|
||
|
});
|
||
|
|
||
|
data.channelManager = {
|
||
|
loggingChannel: Channel.loggingChannel,
|
||
|
loggerParticipant: Channel.loggerParticipant,
|
||
|
channels
|
||
|
};
|
||
|
|
||
|
data.clientManager = {
|
||
|
users,
|
||
|
clients
|
||
|
};
|
||
|
|
||
|
data.uptime = Date.now() - this.server.startTime;
|
||
|
data.config = this.server.config;
|
||
|
if (this.user) {
|
||
|
data.p = {
|
||
|
_id: this.user._id,
|
||
|
name: this.user.name,
|
||
|
color: this.user.color,
|
||
|
flags: this.user.flags
|
||
|
// inventory: this.user.inventory
|
||
|
};
|
||
|
}
|
||
|
|
||
|
this.sendArray([data]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param {Channel} ch
|
||
|
* @param {Client} cl If this is present, only this client's user data will be sent(?)
|
||
|
*/
|
||
|
sendChannelUpdate(ch, cl) {
|
||
|
let msg = ch.fetchChannelData(this, cl);
|
||
|
this.sendArray([msg]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Client;
|