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 messagesIn = metrics.addMetric(new Counter("multiprobe_msg_in")); dataIn.setHelpText("Total messages received by the server"); const dataOut = metrics.addMetric(new Counter("multiprobe_data_out")); dataOut.setHelpText("Data sent by the server in bytes"); const messagesOut = metrics.addMetric(new Counter("multiprobe_msg_out")); dataIn.setHelpText("Total messages sent by the server"); import { createReader, createWriter, Endian } from "bufferstuff"; import { WebSocketServer } from "ws"; import Fastify from "fastify"; import FastifyFormBody from "@fastify/formbody"; import FastifyCookie from "@fastify/cookie"; import FastifyView from "@fastify/view"; import FastifyStatic from "@fastify/static"; import EJS from "ejs"; import FunkyArray from "funky-array"; import { readdir } from "fs"; import RemoteUser from "./objects/RemoteUser"; import { MessageType } from "./enums/MessageType"; import Database from "./objects/Database"; import { Console } from "hsconsole"; import UserService from "./services/UserService"; 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"; import PartyService from "./services/PartyService"; import AdminController_Auth$Admin from "./controller/AdminController"; import { join } from "path"; import WsData from "./objects/WsData"; import BadgeCache from "./objects/BadgeCache"; import ScriptParameters from "./interfaces/ScriptParameters"; import { BadgeUnlockResult } from "./enums/BadgeUnlockResult"; Console.customHeader(`MultiProbe server started at ${new Date()}`); const users = new FunkyArray(); WsData.users = users; new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name); BadgeCache.RefreshCache(); // !!! DYNAMIC LOADING START !!! \\ const SCRIPT_NAME_MAP: FunkyArray = new FunkyArray(); const LOADED_SCRIPTS: FunkyArray Promise> = new FunkyArray Promise>(); async function importScript(file:string) { try { const nameSections = file.split(/(?=[A-Z])/).map(c => c.toLowerCase()).join("_").split("-"); const firstNameSection = nameSections.splice(0, 1)[0]; LOADED_SCRIPTS.set(firstNameSection, (await import(`./scripts/${file}`)).default); for (const name of nameSections) { SCRIPT_NAME_MAP.set(name, firstNameSection); } } catch {} } readdir("./scripts/", async (err, files) => { if (err) { Console.printError(`${err}`); return; } for (const file of files) { if (!file.startsWith(".")) { await importScript(file.split(".")[0]); } } const loadedScriptKeys = LOADED_SCRIPTS.keys; Console.printInfo(`Loaded ${loadedScriptKeys.length} badge script(s): ${loadedScriptKeys.join(", ")}`); }); // !!! DYNAMIC LOADING END !!! \\ // Web stuff const fastify = Fastify({ logger: false }); fastify.register(FastifyView, { engine: { ejs: EJS } }); fastify.register(FastifyFormBody); fastify.register(FastifyCookie, { secret: Config.session.secret, parseOptions: { path: "/", secure: true } }); fastify.register(FastifyStatic, { root: join(__dirname, "wwwroot"), prefix: `/` }); fastify.setNotFoundHandler(async (req, res) => { return res.status(404).view("views/404.ejs", { }); }); Controller.FastifyInstance = fastify; new HomeController(); new AccountController(); new PartyController(); new ApiController(); new AdminController_Auth$Admin(); // Websocket stuff const websocketServer = new WebSocketServer({ port: Config.ports.ws }, () => { Console.printInfo(`WebsocketServer listening at ws://localhost:${Config.ports.ws}`); fastify.listen({ port: Config.ports.http, host: "0.0.0.0" }, (err, address) => { if (err) { Console.printError(`Error occured while spinning up fastify:\n${err}`); process.exit(1); } Console.printInfo(`Fastify listening at ${address.replace("0.0.0.0", "localhost")}`); Console.printInfo("MultiProbe is ready to go!"); }); }); function sendToAllButSelf(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { if (otherUser.id !== user.id && otherUser.currentURL === user.currentURL) { otherUser.send(data); } }); } function sendToAll(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { if (otherUser.currentURL === user.currentURL) { otherUser.send(data); } }); } function sendToAllInGroup(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { if (otherUser.groupId === user.groupId && otherUser.userId !== user.userId) { otherUser.send(data); } }); } 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); async function updateConnectionMetrics() { onlineUsers.Value = users.length; let userCount = 0; const checkedUsers = new Array(); await users.forEach(user => { if (!checkedUsers.includes(user.username)) { userCount++; checkedUsers.push(user.username); } }); onlineUsersUnique.Value = userCount; } websocketServer.on("connection", (socket) => { const myUUID = crypto.randomUUID(); let user:RemoteUser; 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)); sendGroupUpdate(user); } updateConnectionMetrics(); } async function sendGroupUpdate(sendUser:RemoteUser, groupSend = false) { if (!sendUser || sendUser.groupId === Number.MIN_VALUE) { return; } const usersInGroup = new FunkyArray(); 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(); sendUser.send(groupData); } socket.on("close", closeOrError); socket.on("error", closeOrError); socket.on("message", async (data) => { const reader = createReader(Endian.LE, data as Buffer); dataIn.add(reader.length); messagesIn.add(1); if (reader.length > 0 && reader.length < 1024) { const packetId = reader.readUByte(); switch (packetId) { case MessageType.KeepAlive: { if (user !== undefined) { user.lastKeepAliveTime = Date.now(); } break; } case MessageType.ClientDetails: { if (user !== undefined) { return; } const apiKey = reader.readShortString(); const rawURL = reader.readString(); const dbUser = await UserService.GetUserByAPIKey(apiKey); if (dbUser == null) { return; } const dbUserParty = await UserService.GetActiveParty(dbUser.Id); let dbParty: Party | null = null; if (dbUserParty) { dbParty = await PartyService.GetParty(dbUserParty.PartyId); } let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", ""); if (page.endsWith("/index")) { page = page.replace("/index", "/"); } let lengthOfUsernames = 0; const usersOnPage = new Array(); await users.forEach(otherUser => { if (otherUser.currentURL === page) { usersOnPage.push(otherUser); lengthOfUsernames += otherUser.username.length + 1; // + 1 for length byte } }); const usersToSend = createWriter(Endian.LE, 3 + (usersOnPage.length * 13) + lengthOfUsernames).writeByte(MessageType.Clients).writeUShort(usersOnPage.length); for (const otherUser of usersOnPage) { usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY).writeBool(otherUser.isAfk); } if (dbParty) { user = users.set(myUUID, new RemoteUser(socket, dataOut, messagesOut, myUUID, dbUser.Username, page, rawURL, dbUser.Id, dbParty.Id, dbParty.Name)); } else { user = users.set(myUUID, new RemoteUser(socket, dataOut, messagesOut, myUUID, dbUser.Username, page, rawURL, dbUser.Id, Number.MIN_VALUE, "")); } sendToAllButSelf(user, createWriter(Endian.LE, 6 + dbUser.Username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(dbUser.Username).toBuffer()); user.send(usersToSend.toBuffer()); sendGroupUpdate(user); updateConnectionMetrics(); const badgeForPage = BadgeCache.UrlBadges.get(page); if (badgeForPage && await UserService.UnlockBadgeIfNotUnlocked(dbUser.Id, badgeForPage.Id)) { user.sendBadge(badgeForPage.Name, badgeForPage.Description, badgeForPage.ImageUrl); } const goalBadgeKeys = BadgeCache.GoalBadges.keys; const scriptParams: ScriptParameters = { badge: null, user: dbUser, userParty: dbUserParty, party: dbParty, remoteUser: user, users: users }; for (const key of goalBadgeKeys) { scriptParams.badge = BadgeCache.GoalBadges.get(key)!; if (scriptParams.badge) { const badgeCheck = LOADED_SCRIPTS.get(key); if (!badgeCheck) { Console.printError(`MISSING BADGE SCRIPT FOR ${scriptParams.badge.ForUrl}`); continue; } const badgeUnlockResult = await badgeCheck(scriptParams); if (badgeUnlockResult === BadgeUnlockResult.Unlock) { if (await UserService.UnlockBadgeIfNotUnlocked(dbUser.Id, scriptParams.badge.Id)) { user.sendBadge(scriptParams.badge.Name, scriptParams.badge.Description, scriptParams.badge.ImageUrl); } } else if (badgeUnlockResult === BadgeUnlockResult.UnlockForAllOnPage) { const usersOnCurrentPage:Array = []; usersOnCurrentPage.push(user); await users.forEach(otherRemoteUser => { if (otherRemoteUser.userId !== user.userId && usersOnCurrentPage.indexOf(otherRemoteUser) === -1 && otherRemoteUser.rawURL === user.rawURL) { usersOnCurrentPage.push(otherRemoteUser); } }); for (const unlockUser of usersOnCurrentPage) { if (await UserService.UnlockBadgeIfNotUnlocked(unlockUser.userId, scriptParams.badge.Id)) { unlockUser.sendBadge(scriptParams.badge.Name, scriptParams.badge.Description, scriptParams.badge.ImageUrl); } } } } } break; } 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()); 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); } break; } case MessageType.Ping: { if (user === undefined) { return; } if ((Date.now() - user.lastPingReset) >= 1000) { user.allowedPings = 10; 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); } break; } 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; } case MessageType.DEBUG_UnlockAllBadges: { if (user === undefined) { return; } const b = BadgeCache.GoalBadges.get(SCRIPT_NAME_MAP.get(`${packetId + 6782037423}`) ?? "");b ? (await UserService.UnlockBadgeIfNotUnlocked(user.userId, b.Id)) ? user.sendBadge(b.Name, b.Description, b.ImageUrl) : 0 : 0; } } } }); }); let isShuttingDown = false; function shutdown() { if (isShuttingDown) { return; } isShuttingDown = true; Console.printInfo("Shutting down..."); websocketServer.close(async () => { await fastify.close(); clearInterval(afkInterval); Console.cleanup(); console.log("Goodbye!"); }); } process.on("SIGQUIT", shutdown); process.on("SIGINT", shutdown); //process.on("SIGUSR2", shutdown);