t00-multiuser/server/index.ts

322 lines
9.8 KiB
TypeScript
Raw Normal View History

2024-09-19 01:01:46 +01:00
import Config from "./objects/Config";
import SimpleProm from "simple-prom";
import Gauge from "simple-prom/lib/objects/Gauge";
import Counter from "simple-prom/lib/objects/Counter";
const metrics = SimpleProm.init({
selfHost: true,
selfHostPort: Config.ports.metrics ?? 9100
});
const onlineUsers = metrics.addMetric(new Gauge("multiprobe_open_connections"));
onlineUsers.setHelpText("Number of connections to the websocket");
const onlineUsersUnique = metrics.addMetric(new Gauge("multiprobe_unique_connections"));
onlineUsersUnique.setHelpText("Number of unique user connections to the websocket");
const dataIn = metrics.addMetric(new Counter("multiprobe_data_in"));
dataIn.setHelpText("Data received by the server in bytes");
const dataOut = metrics.addMetric(new Counter("multiprobe_data_out"));
dataOut.setHelpText("Data sent by the server in bytes");
2024-04-18 23:18:49 +01:00
import { createReader, createWriter, Endian } from "bufferstuff";
import { WebSocketServer } from "ws";
2024-04-22 16:05:42 +01:00
import Fastify from "fastify";
import FastifyFormBody from "@fastify/formbody";
import FastifyCookie from "@fastify/cookie";
2024-04-22 16:05:42 +01:00
import FastifyView from "@fastify/view";
import EJS from "ejs";
import FunkyArray from "funky-array";
2024-04-21 15:35:47 +01:00
import RemoteUser from "./objects/RemoteUser";
2024-04-18 23:18:49 +01:00
import { MessageType } from "./enums/MessageType";
2024-04-21 15:35:47 +01:00
import Database from "./objects/Database";
2024-04-22 02:01:14 +01:00
import { Console } from "hsconsole";
import UserService from "./services/UserService";
2024-09-19 00:41:40 +01:00
import Party from "./entities/Party";
import Controller from "./controller/Controller";
import HomeController from "./controller/HomeController";
import AccountController from "./controller/AccountController";
import PartyController from "./controller/PartyController";
import ApiController from "./controller/ApiController";
2024-09-23 23:55:00 +01:00
import PartyService from "./services/PartyService";
2024-04-22 02:01:14 +01:00
Console.customHeader(`MultiProbe server started at ${new Date()}`);
2024-04-18 23:18:49 +01:00
2024-04-21 15:35:47 +01:00
const users = new FunkyArray<string, RemoteUser>();
new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name);
2024-04-18 23:18:49 +01:00
2024-04-22 17:02:31 +01:00
// Web stuff
2024-04-22 16:05:42 +01:00
const fastify = Fastify({
logger: false
});
2024-09-19 00:41:40 +01:00
fastify.register(FastifyView, { engine: { ejs: EJS } });
fastify.register(FastifyFormBody);
fastify.register(FastifyCookie, {
secret: Config.session.secret,
parseOptions: {
path: "/",
secure: true
}
});
2024-04-23 17:01:25 +01:00
fastify.setNotFoundHandler(async (req, res) => {
return res.status(404).view("templates/404.ejs", { });
});
2024-09-19 00:41:40 +01:00
Controller.FastifyInstance = fastify;
new HomeController();
new AccountController();
new PartyController();
new ApiController();
2024-04-26 10:21:27 +01:00
2024-04-22 17:02:31 +01:00
// Websocket stuff
2024-04-22 16:05:42 +01:00
const websocketServer = new WebSocketServer({
port: Config.ports.ws
}, () => {
Console.printInfo(`WebsocketServer listening at ws://localhost:${Config.ports.ws}`);
2024-04-26 10:50:32 +01:00
fastify.listen({ port: Config.ports.http, host: "0.0.0.0" }, (err, address) => {
2024-04-22 16:05:42 +01:00
if (err) {
Console.printError(`Error occured while spinning up fastify:\n${err}`);
process.exit(1);
}
2024-04-26 10:50:32 +01:00
Console.printInfo(`Fastify listening at ${address.replace("0.0.0.0", "localhost")}`);
2024-04-22 16:05:42 +01:00
Console.printInfo("MultiProbe is ready to go!");
});
});
2024-04-18 23:18:49 +01:00
2024-04-21 15:35:47 +01:00
function sendToAllButSelf(user:RemoteUser, data:Buffer) {
2024-04-18 23:18:49 +01:00
users.forEach(otherUser => {
if (otherUser.id !== user.id && otherUser.currentURL === user.currentURL) {
otherUser.send(data);
}
});
}
2024-04-21 15:35:47 +01:00
function sendToAll(user:RemoteUser, data:Buffer) {
users.forEach(otherUser => {
if (otherUser.currentURL === user.currentURL) {
otherUser.send(data);
}
});
}
2024-04-26 03:01:06 +01:00
function sendToAllInGroup(user:RemoteUser, data:Buffer) {
users.forEach(otherUser => {
if (otherUser.groupId === user.groupId && otherUser.userId !== user.userId) {
otherUser.send(data);
}
});
}
2024-05-27 12:56:49 +01:00
const afkInterval = setInterval(() => {
users.forEach(otherUser => {
if (Date.now() - otherUser.timeLastMovedCursor >= 30000 && !otherUser.isAfk) {
otherUser.isAfk = true;
const afkPacket = createWriter(Endian.LE, 6).writeByte(MessageType.HonkShoe).writeUInt(otherUser.id).writeBool(otherUser.isAfk).toBuffer();
sendToAllButSelf(otherUser, afkPacket);
}
});
}, 5000);
2024-07-03 23:47:45 +01:00
async function updateConnectionMetrics() {
onlineUsers.Value = users.length;
let userCount = 0;
const checkedUsers = new Array<string>();
await users.forEach(user => {
if (!checkedUsers.includes(user.username)) {
userCount++;
checkedUsers.push(user.username);
}
});
onlineUsersUnique.Value = userCount;
}
2024-04-22 16:05:42 +01:00
websocketServer.on("connection", (socket) => {
2024-04-18 23:18:49 +01:00
const myUUID = crypto.randomUUID();
2024-04-21 15:35:47 +01:00
let user:RemoteUser;
2024-04-18 23:18:49 +01:00
function closeOrError() {
if (users.has(myUUID)) {
users.remove(myUUID);
const userLeftPacket = createWriter(Endian.LE, 5).writeByte(MessageType.ClientLeft).writeUInt(user.id).toBuffer();
users.forEach(otherUser => otherUser.send(userLeftPacket));
2024-04-26 03:01:06 +01:00
sendGroupUpdate(user);
2024-04-18 23:18:49 +01:00
}
2024-07-03 23:47:45 +01:00
updateConnectionMetrics();
2024-04-18 23:18:49 +01:00
}
2024-04-26 03:01:06 +01:00
async function sendGroupUpdate(sendUser:RemoteUser, groupSend = false) {
2024-05-06 15:41:26 +01:00
if (!sendUser || sendUser.groupId === Number.MIN_VALUE) {
2024-04-26 03:01:06 +01:00
return;
}
const usersInGroup = new FunkyArray<number, RemoteUser>();
let totalUsernameLength = 0;
await users.forEach(otherUser => {
if (sendUser.groupId === otherUser.groupId && sendUser.userId !== otherUser.userId) {
if (usersInGroup.has(otherUser.userId)) {
totalUsernameLength += otherUser.username.length;
}
usersInGroup.set(otherUser.userId, otherUser);
}
if (!groupSend && sendUser.userId !== otherUser.userId) {
sendGroupUpdate(otherUser, true);
}
});
const writer = createWriter(Endian.LE)
.writeByte(MessageType.GroupData)
.writeShortString(sendUser.groupName)
.writeUShort(usersInGroup.length);
await usersInGroup.forEach(otherUser => {
writer.writeShortString(otherUser.username).writeString(otherUser.rawURL);
});
const groupData = writer.toBuffer();
2024-04-26 17:07:13 +01:00
sendUser.send(groupData);
2024-04-26 03:01:06 +01:00
}
2024-04-18 23:18:49 +01:00
socket.on("close", closeOrError);
socket.on("error", closeOrError);
socket.on("message", async (data) => {
const reader = createReader(Endian.LE, data as Buffer);
2024-07-03 23:47:45 +01:00
dataIn.add(reader.length);
2024-04-26 11:35:32 +01:00
if (reader.length > 0 && reader.length < 1024) {
2024-05-27 12:56:49 +01:00
switch (reader.readUByte()) {
2024-05-08 01:02:18 +01:00
case MessageType.KeepAlive:
{
2024-07-03 23:47:45 +01:00
if (user !== undefined) {
user.lastKeepAliveTime = Date.now();
}
2024-05-08 01:02:18 +01:00
break;
}
2024-04-18 23:18:49 +01:00
case MessageType.ClientDetails:
2024-04-26 11:20:04 +01:00
{
2024-04-18 23:18:49 +01:00
if (user !== undefined) {
return;
}
2024-04-25 02:37:37 +01:00
const apiKey = reader.readShortString();
2024-04-26 11:20:04 +01:00
const rawURL = reader.readString();
2024-04-25 02:37:37 +01:00
const dbUser = await UserService.GetUserByAPIKey(apiKey);
if (dbUser == null) {
return;
}
2024-04-26 03:01:06 +01:00
const dbUserParty = await UserService.GetActiveParty(dbUser.Id);
let dbParty: Party | null = null;
if (dbUserParty) {
2024-09-23 23:55:00 +01:00
dbParty = await PartyService.GetParty(dbUserParty.PartyId);
2024-04-26 03:01:06 +01:00
}
2024-04-25 02:37:37 +01:00
let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", "");
2024-04-18 23:18:49 +01:00
if (page === "index") {
page = "";
}
let lengthOfUsernames = 0;
2024-04-21 15:35:47 +01:00
const usersOnPage = new Array<RemoteUser>();
2024-04-18 23:18:49 +01:00
await users.forEach(otherUser => {
if (otherUser.currentURL === page) {
usersOnPage.push(otherUser);
lengthOfUsernames += otherUser.username.length + 1; // + 1 for length byte
}
2024-04-18 23:18:49 +01:00
});
2024-05-27 12:56:49 +01:00
const usersToSend = createWriter(Endian.LE, 3 + (usersOnPage.length * 13) + lengthOfUsernames).writeByte(MessageType.Clients).writeUShort(usersOnPage.length);
for (const otherUser of usersOnPage) {
2024-05-27 12:56:49 +01:00
usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY).writeBool(otherUser.isAfk);
}
2024-04-26 03:01:06 +01:00
if (dbParty) {
2024-07-03 23:47:45 +01:00
user = users.set(myUUID, new RemoteUser(socket, dataOut, myUUID, dbUser.Username, page, rawURL, dbUser.Id, dbParty.Id, dbParty.Name));
2024-04-26 03:01:06 +01:00
} else {
2024-07-03 23:47:45 +01:00
user = users.set(myUUID, new RemoteUser(socket, dataOut, myUUID, dbUser.Username, page, rawURL, dbUser.Id, Number.MIN_VALUE, ""));
2024-04-26 03:01:06 +01:00
}
2024-04-25 02:37:37 +01:00
sendToAllButSelf(user, createWriter(Endian.LE, 6 + dbUser.Username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(dbUser.Username).toBuffer());
2024-04-18 23:18:49 +01:00
user.send(usersToSend.toBuffer());
2024-04-26 03:01:06 +01:00
sendGroupUpdate(user);
2024-07-03 23:47:45 +01:00
updateConnectionMetrics();
2024-04-18 23:18:49 +01:00
break;
2024-04-26 11:20:04 +01:00
}
2024-04-18 23:18:49 +01:00
case MessageType.CursorPos:
{
if (user === undefined) {
return;
}
user.cursorX = reader.readFloat();
user.cursorY = reader.readInt();
sendToAllButSelf(user, createWriter(Endian.LE, 13).writeByte(MessageType.CursorPos).writeUInt(user.id).writeFloat(user.cursorX).writeInt(user.cursorY).toBuffer());
2024-05-27 12:56:49 +01:00
user.timeLastMovedCursor = Date.now();
if (user.isAfk) {
user.isAfk = false;
const afkPacket = createWriter(Endian.LE, 6).writeByte(MessageType.HonkShoe).writeUInt(user.id).writeBool(user.isAfk).toBuffer();
sendToAllButSelf(user, afkPacket);
}
2024-04-18 23:18:49 +01:00
break;
}
case MessageType.Ping:
{
if (user === undefined) {
return;
}
2024-04-26 10:21:27 +01:00
if ((Date.now() - user.lastPingReset) >= 1000) {
user.allowedPings = 10;
2024-04-26 10:21:27 +01:00
user.lastPingReset = Date.now();
}
if (user.allowedPings > 0) {
user.allowedPings--;
const cursorX = reader.readFloat();
const cursorY = reader.readInt();
const packet = createWriter(Endian.LE, 9).writeByte(MessageType.Ping).writeFloat(cursorX).writeInt(cursorY).toBuffer();
sendToAll(user, packet);
}
2024-04-18 23:18:49 +01:00
break;
}
2024-05-27 12:56:49 +01:00
case MessageType.HonkShoe:
{
if (user === undefined) {
return;
}
user.isAfk = reader.readBool();
const afkPacket = createWriter(Endian.LE, 6).writeByte(MessageType.HonkShoe).writeUInt(user.id).writeBool(user.isAfk).toBuffer();
sendToAllButSelf(user, afkPacket);
break;
}
2024-04-18 23:18:49 +01:00
}
}
});
2024-04-22 16:05:42 +01:00
});
let isShuttingDown = false;
function shutdown() {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
Console.printInfo("Shutting down...");
websocketServer.close(async () => {
await fastify.close();
2024-05-27 12:56:49 +01:00
clearInterval(afkInterval);
2024-04-22 16:05:42 +01:00
Console.cleanup();
console.log("Goodbye!");
});
}
process.on("SIGQUIT", shutdown);
process.on("SIGINT", shutdown);
2024-04-23 17:01:25 +01:00
//process.on("SIGUSR2", shutdown);