diff --git a/Binato.ts b/Binato.ts index 9ca2e43..f476c85 100644 --- a/Binato.ts +++ b/Binato.ts @@ -1,12 +1,13 @@ -import { Application } from "express"; +import { ChatHistory } from "./server/ChatHistory"; import compression from "compression"; +import config from "./config.json"; import { ConsoleHelper } from "./ConsoleHelper"; import express from "express"; -import { readFile } from "fs"; +import { HandleRequest } from "./server/BanchoServer"; +import { readFileSync } from "fs"; import { Registry, collectDefaultMetrics } from "prom-client"; -const binatoApp:Application = express(); -const config = require("./config.json"); +const binatoApp:express.Application = express(); if (config["prometheus"]["enabled"]) { const register:Registry = new Registry(); @@ -14,7 +15,7 @@ if (config["prometheus"]["enabled"]) { collectDefaultMetrics({ register }); - const prometheusApp:Application = express(); + const prometheusApp:express.Application = express(); prometheusApp.get("/metrics", async (req, res) => { res.end(await register.metrics()); }); @@ -31,6 +32,8 @@ if (config["express"]["compression"]) { ConsoleHelper.printWarn("Compression is disabled"); } +const INDEX_PAGE:string = readFileSync("./web/serverPage.html").toString(); + binatoApp.use((req, res) => { let packet:Buffer = Buffer.alloc(0); req.on("data", (chunk:Buffer) => packet = Buffer.concat([packet, chunk], packet.length + chunk.length)); @@ -38,21 +41,9 @@ binatoApp.use((req, res) => { switch (req.method) { case "GET": if (req.url == "/" || req.url == "/index.html" || req.url == "/index") { - res.sendFile(`${__dirname}/web/serverPage.html`); + res.send(INDEX_PAGE); } else if (req.url == "/chat") { - readFile("./web/chatPageTemplate.html", (err, data) => { - if (err) throw err; - - let lines = "", flip = false; - const limit = global.chatHistory.length < 10 ? 10 : global.chatHistory.length; - for (let i = global.chatHistory.length - 10; i < limit; i++) { - if (i < 0) i = 0; - lines += `
${global.chatHistory[i] == null ? "blank" : global.chatHistory[i]}
` - flip = !flip; - } - - res.send(data.toString().replace("|content|", lines)); - }); + res.send(ChatHistory.GenerateForWeb()); } break; @@ -60,8 +51,8 @@ binatoApp.use((req, res) => { // Make sure this address should respond to bancho requests // Bancho addresses: c, c1, c2, c3, c4, c5, c6, ce // Just looking for the first character being "c" *should* be enough - if (req.headers["host"].split(".")[0][0] == "c") - serverHandler(req, res); + if (req.headers.host != null && req.headers.host.split(".")[0][0] == "c") + HandleRequest(req, res, packet); else res.status(400).send("400 | Bad Request!
Binato only accepts POST requests on Bancho subdomains.
Binato"); break; diff --git a/package-lock.json b/package-lock.json index 9547078..630b65c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@types/compression": "^1.7.2", "@types/express": "^4.17.14", "@types/node": "^18.11.9", "chalk": "^4.1.0", @@ -16,7 +17,8 @@ "express": "^4.18.2", "mysql2": "^2.3.3", "osu-packet": "^4.1.2", - "prom-client": "^14.1.0" + "prom-client": "^14.1.0", + "redis": "^4.5.0" }, "devDependencies": { "nodemon": "^2.0.20", @@ -60,6 +62,59 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@redis/bloom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.1.0.tgz", + "integrity": "sha512-9QovlxmpRtvxVbN0UBcv8WfdSMudNZZTFqCsnBszcQXqaZb/TVe30ScgGEO7u1EAIacTPAo7/oCYjYAxiHLanQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.4.0.tgz", + "integrity": "sha512-1gEj1AkyXPlkcC/9/T5xpDcQF8ntERURjLBgEWMTdUZqe181zfI9BY3jc2OzjTLkvZh5GV7VT4ktoJG2fV2ufw==", + "dependencies": { + "cluster-key-slot": "1.1.1", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", + "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz", + "integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", + "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -93,6 +148,14 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", + "integrity": "sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -374,6 +437,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -656,6 +727,14 @@ "is-property": "^1.0.2" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -1171,6 +1250,19 @@ "node": ">=8.10.0" } }, + "node_modules/redis": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.5.0.tgz", + "integrity": "sha512-oZGAmOKG+RPnHo0UxM5GGjJ0dBd/Vi4fs3MYwM1p2baDoXC0wpm0yOdpxVS9K+0hM84ycdysp2eHg2xGoQ4FEw==", + "dependencies": { + "@redis/bloom": "1.1.0", + "@redis/client": "1.4.0", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.0", + "@redis/time-series": "1.0.4" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1510,6 +1602,46 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@redis/bloom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.1.0.tgz", + "integrity": "sha512-9QovlxmpRtvxVbN0UBcv8WfdSMudNZZTFqCsnBszcQXqaZb/TVe30ScgGEO7u1EAIacTPAo7/oCYjYAxiHLanQ==", + "requires": {} + }, + "@redis/client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.4.0.tgz", + "integrity": "sha512-1gEj1AkyXPlkcC/9/T5xpDcQF8ntERURjLBgEWMTdUZqe181zfI9BY3jc2OzjTLkvZh5GV7VT4ktoJG2fV2ufw==", + "requires": { + "cluster-key-slot": "1.1.1", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + } + }, + "@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "requires": {} + }, + "@redis/json": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", + "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "requires": {} + }, + "@redis/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz", + "integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==", + "requires": {} + }, + "@redis/time-series": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", + "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "requires": {} + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1543,6 +1675,14 @@ "@types/node": "*" } }, + "@types/compression": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", + "integrity": "sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==", + "requires": { + "@types/express": "*" + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -1766,6 +1906,11 @@ "readdirp": "~3.6.0" } }, + "cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1988,6 +2133,11 @@ "is-property": "^1.0.2" } }, + "generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==" + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -2377,6 +2527,19 @@ "picomatch": "^2.2.1" } }, + "redis": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.5.0.tgz", + "integrity": "sha512-oZGAmOKG+RPnHo0UxM5GGjJ0dBd/Vi4fs3MYwM1p2baDoXC0wpm0yOdpxVS9K+0hM84ycdysp2eHg2xGoQ4FEw==", + "requires": { + "@redis/bloom": "1.1.0", + "@redis/client": "1.4.0", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.0", + "@redis/time-series": "1.0.4" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/package.json b/package.json index 0b11669..5b1aa0f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "author": "", "license": "MIT", "dependencies": { + "@types/compression": "^1.7.2", "@types/express": "^4.17.14", "@types/node": "^18.11.9", "chalk": "^4.1.0", @@ -17,7 +18,8 @@ "express": "^4.18.2", "mysql2": "^2.3.3", "osu-packet": "^4.1.2", - "prom-client": "^14.1.0" + "prom-client": "^14.1.0", + "redis": "^4.5.0" }, "devDependencies": { "nodemon": "^2.0.20", diff --git a/server/BanchoServer.ts b/server/BanchoServer.ts index 92ac648..8c35cb2 100644 --- a/server/BanchoServer.ts +++ b/server/BanchoServer.ts @@ -1,71 +1,76 @@ -import * as osu from "osu-packet"; +import config from "../config.json"; import { ConsoleHelper } from "../ConsoleHelper"; +import { Database } from "./objects/Database"; +import { UserArray } from "./objects/UserArray"; +import { LatLng } from "./objects/LatLng"; import { Packets } from "./enums/Packets"; +import { RedisClientType, createClient } from "redis"; +import { replaceAll } from "./Util"; +import { Request, Response } from "express"; +import { User } from "./objects/User"; +import * as osu from "osu-packet"; -const +/*const loginHandler = require("./loginHandler.js"), parseUserData = require("./util/parseUserData.js"), - User = require("./User.js"), getUserFromToken = require("./util/getUserByToken.js"), getUserById = require("./util/getUserById.js"), bakedResponses = require("./bakedResponses.js"), - Streams = require("./Streams.js"), - DatabaseHelperClass = require("./DatabaseHelper.js"), - funkyArray = require("./util/funkyArray.js"), - config = require("../config.json"); + Streams = require("./Streams.js");*/ -// Users funkyArray for session storage -global.users = new funkyArray(); - -// Add the bot user -global.botUser = global.users.add("bot", new User(3, "SillyBot", "bot")); -// Set the bot's position on the map -global.botUser.location[0] = 50; -global.botUser.location[1] = -32; - -global.DatabaseHelper = new DatabaseHelperClass(config.database.address, config.database.port, config.database.username, config.database.password, config.database.name, async () => { +const DB:Database = new Database(config.database.address, config.database.port, config.database.username, config.database.password, config.database.name, async () => { // Close any unclosed db matches on startup - global.DatabaseHelper.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL"); - global.DatabaseHelper.query("UPDATE osu_info SET value = 0 WHERE name = 'online_now'"); + DB.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL"); + DB.query("UPDATE osu_info SET value = 0 WHERE name = 'online_now'"); }); -async function subscribeToChannel(channelName = "", callback = function(message = "") {}) { +// Users funkyArray for session storage +const users = new UserArray(); + +// Add the bot user +const botUser:User = users.add("bot", new User(3, "SillyBot", "bot", DB)); +// Set the bot's position on the map +botUser.location = new LatLng(50, -32); + +let redisClient:RedisClientType; + +async function subscribeToChannel(channelName:string, callback:(message:string) => void) { // Dup and connect new client for channel subscription (required) - const subscriptionClient = global.promClient.duplicate(); + const subscriptionClient:RedisClientType = redisClient.duplicate(); await subscriptionClient.connect(); // Subscribe to channel await subscriptionClient.subscribe(channelName, callback); ConsoleHelper.printRedis(`Subscribed to ${channelName} channel`); } -// Do redis if it's enabled if (config.redis.enabled) { (async () => { - const { createClient } = require("redis"); - global.promClient = createClient({ - url: `redis://${config.redis.password.replaceAll(" ", "") == "" ? "" : `${config.redis.password}@`}${config.redis.address}:${config.redis.port}/${config.redis.database}` + redisClient = createClient({ + url: `redis://${replaceAll(config.redis.password, " ", "") == "" ? "" : `${config.redis.password}@`}${config.redis.address}:${config.redis.port}/${config.redis.database}` }); - global.promClient.on('error', e => consoleHelper.printRedis(e)); + redisClient.on('error', e => ConsoleHelper.printRedis(e)); const connectionStartTime = Date.now(); - await global.promClient.connect(); - consoleHelper.printRedis(`Connected to redis server. Took ${Date.now() - connectionStartTime}ms`); + await redisClient.connect(); + ConsoleHelper.printRedis(`Connected to redis server. Took ${Date.now() - connectionStartTime}ms`); // Score submit update channel subscribeToChannel("binato:update_user_stats", (message) => { - const user = getUserById(parseInt(message)); - // Update user info - user.updateUserInfo(true); + if (typeof(message) === "string") { + const user = users.getById(parseInt(message)); + // Update user info + user.updateUserInfo(true); - consoleHelper.printRedis(`Score submission stats update request received for ${user.username}`); + ConsoleHelper.printRedis(`Score submission stats update request received for ${user.username}`); + } }); })(); -} else consoleHelper.printWarn("Redis is disabled!"); +} else ConsoleHelper.printWarn("Redis is disabled!"); // User timeout interval setInterval(() => { - for (let User of global.users.getIterableItems()) { + for (let User of users.getIterableItems()) { if (User.id == 3) continue; // Ignore the bot // Bot: :( @@ -75,36 +80,25 @@ setInterval(() => { } }, 10000); -// An array containing the last 15 messages in chat -global.chatHistory = []; -global.addChatMessage = function(msg) { - if (global.chatHistory.length == 15) { - global.chatHistory.splice(0, 1); - global.chatHistory.push(msg); - } else { - global.chatHistory.push(msg); - } -} - // Init stream class -Streams.init(); +//Streams.init(); // An array containing all chat channels -global.channels = [ +/*global.channels = [ { channelName:"#osu", channelTopic:"The main channel", channelUserCount: 0, locked: false }, { channelName:"#userlog", channelTopic:"Log about stuff doing go on yes very", channelUserCount: 0, locked: false }, { channelName:"#lobby", channelTopic:"Talk about multiplayer stuff", channelUserCount: 0, locked: false }, { channelName:"#english", channelTopic:"Talk in exclusively English", channelUserCount: 0, locked: false }, { channelName:"#japanese", channelTopic:"Talk in exclusively Japanese", channelUserCount: 0, locked: false }, -]; +];*/ // Create a stream for each chat channel -for (let i = 0; i < global.channels.length; i++) { +/*for (let i = 0; i < global.channels.length; i++) { Streams.addStream(global.channels[i].channelName, false); -} +}*/ // Add a stream for the multiplayer lobby -Streams.addStream("multiplayer_lobby", false); +//Streams.addStream("multiplayer_lobby", false); // Include packets const ChangeAction = require("./Packets/ChangeAction.js"), @@ -127,11 +121,11 @@ const ChangeAction = require("./Packets/ChangeAction.js"), TourneyMatchLeaveChannel = require("./Packets/TourneyLeaveMatchChannel.js"); // A class for managing everything multiplayer -global.MultiplayerManager = new MultiplayerManager(); +const multiplayerManager:MultiplayerManager = new MultiplayerManager(); -module.exports = async function(req, res, packet:Buffer) { +export async function HandleRequest(req:Request, res:Response, packet:Buffer) { // Get the client's token string and request data - const requestTokenString:string = req.header("osu-token"), + const requestTokenString:string | undefined = req.header("osu-token"), requestData:Buffer = packet; // Server's response @@ -147,7 +141,7 @@ module.exports = async function(req, res, packet:Buffer) { // Client has a token, let's see what they want. try { // Get the current user - const PacketUser:User = getUserFromToken(requestTokenString); + const PacketUser:User = users.getByToken(requestTokenString); // Make sure the client's token isn't invalid if (PacketUser != null) { @@ -160,7 +154,7 @@ module.exports = async function(req, res, packet:Buffer) { const PacketData = osuPacketReader.Parse(); // Go through each packet sent by the client - for (CurrentPacket of PacketData) { + for (let CurrentPacket of PacketData) { switch (CurrentPacket.id) { case Packets.Client_ChangeAction: ChangeAction(PacketUser, CurrentPacket.data); @@ -190,136 +184,136 @@ module.exports = async function(req, res, packet:Buffer) { Spectator.stopSpectatingUser(PacketUser); break; - case Packets.client_sendPrivateMessage: + case Packets.Client_sendPrivateMessage: SendPrivateMessage(PacketUser, CurrentPacket.data); break; - case Packets.client_joinLobby: + case Packets.Client_joinLobby: global.MultiplayerManager.userEnterLobby(PacketUser); break; - case Packets.client_partLobby: + case Packets.Client_partLobby: global.MultiplayerManager.userLeaveLobby(PacketUser); break; - case Packets.client_createMatch: + case Packets.Client_createMatch: await global.MultiplayerManager.createMultiplayerMatch(PacketUser, CurrentPacket.data); break; - case Packets.client_joinMatch: + case Packets.Client_joinMatch: global.MultiplayerManager.joinMultiplayerMatch(PacketUser, CurrentPacket.data); break; - case Packets.client_matchChangeSlot: + case Packets.Client_matchChangeSlot: PacketUser.currentMatch.moveToSlot(PacketUser, CurrentPacket.data); break; - case Packets.client_matchReady: + case Packets.Client_matchReady: PacketUser.currentMatch.setStateReady(PacketUser); break; - case Packets.client_matchChangeSettings: + case Packets.Client_matchChangeSettings: await PacketUser.currentMatch.updateMatch(PacketUser, CurrentPacket.data); break; - case Packets.client_matchNotReady: + case Packets.Client_matchNotReady: PacketUser.currentMatch.setStateNotReady(PacketUser); break; - case Packets.client_partMatch: + case Packets.Client_partMatch: await global.MultiplayerManager.leaveMultiplayerMatch(PacketUser); break; // Also handles user kick if the slot has a user - case Packets.client_matchLock: + case Packets.Client_matchLock: PacketUser.currentMatch.lockMatchSlot(PacketUser, CurrentPacket.data); break; - case Packets.client_matchNoBeatmap: + case Packets.Client_matchNoBeatmap: PacketUser.currentMatch.missingBeatmap(PacketUser); break; - case Packets.client_matchSkipRequest: + case Packets.Client_matchSkipRequest: PacketUser.currentMatch.matchSkip(PacketUser); break; - case Packets.client_matchHasBeatmap: + case Packets.Client_matchHasBeatmap: PacketUser.currentMatch.notMissingBeatmap(PacketUser); break; - case Packets.client_matchTransferHost: + case Packets.Client_matchTransferHost: PacketUser.currentMatch.transferHost(PacketUser, CurrentPacket.data); break; - case Packets.client_matchChangeMods: + case Packets.Client_matchChangeMods: PacketUser.currentMatch.updateMods(PacketUser, CurrentPacket.data); break; - case Packets.client_matchStart: + case Packets.Client_matchStart: PacketUser.currentMatch.startMatch(); break; - case Packets.client_matchLoadComplete: + case Packets.Client_matchLoadComplete: PacketUser.currentMatch.matchPlayerLoaded(PacketUser); break; - case Packets.client_matchComplete: + case Packets.Client_matchComplete: await PacketUser.currentMatch.onPlayerFinishMatch(PacketUser); break; - case Packets.client_matchScoreUpdate: + case Packets.Client_matchScoreUpdate: PacketUser.currentMatch.updatePlayerScore(PacketUser, CurrentPacket.data); break; - case Packets.client_matchFailed: + case Packets.Client_matchFailed: PacketUser.currentMatch.matchFailed(PacketUser); break; - case Packets.client_matchChangeTeam: + case Packets.Client_matchChangeTeam: PacketUser.currentMatch.changeTeam(PacketUser); break; - case Packets.client_channelJoin: + case Packets.Client_channelJoin: ChannelJoin(PacketUser, CurrentPacket.data); break; - case Packets.client_channelPart: + case Packets.Client_channelPart: ChannelPart(PacketUser, CurrentPacket.data); break; - case Packets.client_setAwayMessage: + case Packets.Client_setAwayMessage: SetAwayMessage(PacketUser, CurrentPacket.data); break; - case Packets.client_friendAdd: + case Packets.Client_friendAdd: AddFriend(PacketUser, CurrentPacket.data); break; - case Packets.client_friendRemove: + case Packets.Client_friendRemove: RemoveFriend(PacketUser, CurrentPacket.data); break; - case Packets.client_userStatsRequest: + case Packets.Client_userStatsRequest: UserStatsRequest(PacketUser, CurrentPacket.data); break; - case Packets.client_specialMatchInfoRequest: + case Packets.Client_specialMatchInfoRequest: TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data); break; - case Packets.client_specialJoinMatchChannel: + case Packets.Client_specialJoinMatchChannel: TourneyMatchJoinChannel(PacketUser, CurrentPacket.data); break; - case Packets.client_specialLeaveMatchChannel: + case Packets.Client_specialLeaveMatchChannel: TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data); break; - case Packets.client_invite: + case Packets.Client_invite: MultiplayerInvite(PacketUser, CurrentPacket.data); break; - case Packets.client_userPresenceRequest: + case Packets.Client_userPresenceRequest: UserPresence(PacketUser, PacketUser.id); // Can't really think of a way to generalize this? break; diff --git a/server/ChatHistory.ts b/server/ChatHistory.ts new file mode 100644 index 0000000..84397a6 --- /dev/null +++ b/server/ChatHistory.ts @@ -0,0 +1,34 @@ +import { readFileSync } from "fs"; + +export abstract class ChatHistory { + private static _history:Array = new Array(); + private static _lastGeneratedPage:string; + private static _hasChanged:boolean = true; + private static readonly HISTORY_LENGTH = 10; + private static readonly PAGE_TEMPLATE = readFileSync("./web/chatPageTemplate.html").toString(); + + public static AddMessage(message:string) : void { + if (this._history.length === 10) { + this._history.splice(0, 1); + } + + this._history.push(message); + this._hasChanged = true; + } + + public static GenerateForWeb() : string { + let lines:string = "", flip:boolean = false; + + for (let i:number = Math.max(this._history.length - this.HISTORY_LENGTH, this.HISTORY_LENGTH); i < this._history.length; i++) { + lines += `
${this._history[i] == null ? "blank" : this._history[i]}
` + flip = !flip; + } + + if (this._hasChanged) { + this._lastGeneratedPage = this.PAGE_TEMPLATE.toString().replace("|content|", lines); + this._hasChanged = false; + } + + return this._lastGeneratedPage; + } +} \ No newline at end of file diff --git a/server/Util.ts b/server/Util.ts new file mode 100644 index 0000000..7f1f806 --- /dev/null +++ b/server/Util.ts @@ -0,0 +1,8 @@ +// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions +function escapeRegExp(string:string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +export function replaceAll(inputString:string, toReplace:string, toReplaceWith:string) { + return inputString.replace(`/:${escapeRegExp(toReplace)}:/g`, toReplaceWith); +} \ No newline at end of file diff --git a/server/enums/Packets.ts b/server/enums/Packets.ts index aa19f01..8a5eb05 100644 --- a/server/enums/Packets.ts +++ b/server/enums/Packets.ts @@ -56,7 +56,7 @@ export enum Packets { Client_MatchNoBeatmap, Client_MatchNotReady, Client_MatchFailed, - Server_MatchComplete, + Server_MatchComplete = 58, Client_MatchHasBeatmap, Client_MatchSkipRequest, Server_MatchSkip, @@ -109,5 +109,5 @@ export enum Packets { // NOTE: Tournament client only Client_SpecialJoinMatchChannel, // NOTE: Tournament client only - Client_SpecialLeaveMatchChanne, + Client_SpecialLeaveMatchChannel, } \ No newline at end of file diff --git a/server/enums/RankingModes.ts b/server/enums/RankingModes.ts new file mode 100644 index 0000000..22f135e --- /dev/null +++ b/server/enums/RankingModes.ts @@ -0,0 +1,5 @@ +export enum RankingModes { + PP, + RANKED_SCORE, + AVG_ACCURACY +}; \ No newline at end of file diff --git a/server/objects/Database.ts b/server/objects/Database.ts new file mode 100644 index 0000000..f4df199 --- /dev/null +++ b/server/objects/Database.ts @@ -0,0 +1,77 @@ +import { ConsoleHelper } from "../../ConsoleHelper"; +import { createPool, Pool } from "mysql2"; + +export class Database { + private connectionPool:Pool; + private static readonly CONNECTION_LIMIT = 128; + + public constructor(databaseAddress:string, databasePort:number = 3306, databaseUsername:string, databasePassword:string, databaseName:string, connectedCallback:Function) { + this.connectionPool = createPool({ + connectionLimit: Database.CONNECTION_LIMIT, + host: databaseAddress, + port: databasePort, + user: databaseUsername, + password: databasePassword, + database: databaseName + }); + + const classCreationTime:number = Date.now(); + const connectionCheckInterval = setInterval(() => { + this.query("SELECT name FROM osu_info LIMIT 1") + .then(data => { + ConsoleHelper.printBancho(`Connected to database. Took ${Date.now() - classCreationTime}ms`); + clearInterval(connectionCheckInterval); + + connectedCallback(); + }) + .catch(err => {}); + }, 17); // Roughly 6 times per sec + } + + public query(query = "", data?:Array) { + const limited = query.includes("LIMIT 1"); + + return new Promise((resolve, reject) => { + this.connectionPool.getConnection((err, connection) => { + if (err) { + reject(err); + try { + connection.release(); + } catch (e) { + ConsoleHelper.printError("Failed to release mysql connection\n" + err); + } + } else { + // Use old query + if (data == null) { + connection.query(query, (err, data) => { + if (err) { + reject(err); + connection.release(); + } else { + dataReceived(resolve, data, limited); + connection.release(); + } + }); + } + // Use new prepared statements w/ placeholders + else { + connection.execute(query, data, (err, data) => { + if (err) { + reject(err); + connection.release(); + } else { + dataReceived(resolve, data, limited); + connection.release(); + } + }); + } + } + }); + }); + } +} + +function dataReceived(resolveCallback:(value:unknown) => void, data:any, limited:boolean = false) : void { + if (limited) resolveCallback(data[0]); + else resolveCallback(data); +} \ No newline at end of file diff --git a/server/objects/FunkyArray.ts b/server/objects/FunkyArray.ts new file mode 100644 index 0000000..6806802 --- /dev/null +++ b/server/objects/FunkyArray.ts @@ -0,0 +1,74 @@ +export class FunkyArray { + private items:any = {}; + private itemKeys:Array = Object.keys(this.items); + private iterableArray:Array = new Array(); + + public add(uuid:string, item:T, regenerate:boolean = true) : T { + this.items[uuid] = item; + + if (regenerate) { + this.itemKeys = Object.keys(this.items); + this.regenerateIterableArray(); + } + + return this.items[uuid]; + } + + public remove(uuid:string, regenerate:boolean = true) { + delete this.items[uuid]; + if (regenerate) { + this.itemKeys = Object.keys(this.items); + this.regenerateIterableArray(); + } + } + + public removeFirstItem(regenerate:boolean = true) : void { + delete this.items[this.itemKeys[0]]; + this.itemKeys = Object.keys(this.items); + if (regenerate) this.regenerateIterableArray(); + } + + public regenerateIterableArray() : void { + this.iterableArray = new Array(); + for (let itemKey of this.itemKeys) { + this.iterableArray.push(this.items[itemKey]); + } + this.itemKeys = Object.keys(this.items); + } + + public getFirstItem() : T { + return this.items[this.itemKeys[0]]; + } + + public getLength() : number { + return this.itemKeys.length; + } + + public getKeyById(id:number) : string { + return this.itemKeys[id]; + } + + public getById(id:number) : T | undefined { + return this.items[this.itemKeys[id]]; + } + + public getByKey(key:string) : T | undefined { + if (key in this.items) { + return this.items[key]; + } + + return undefined; + } + + public getKeys() : Array { + return this.itemKeys; + } + + public getItems() : any { + return this.items; + } + + public getIterableItems() : Array { + return this.iterableArray; + } +} \ No newline at end of file diff --git a/server/objects/LatLng.ts b/server/objects/LatLng.ts new file mode 100644 index 0000000..8a494d7 --- /dev/null +++ b/server/objects/LatLng.ts @@ -0,0 +1,9 @@ +export class LatLng { + public latitude:number; + public longitude:number; + + public constructor(latitude:number, longitude:number) { + this.latitude = latitude; + this.longitude = longitude; + } +} \ No newline at end of file diff --git a/server/objects/User.ts b/server/objects/User.ts index e69de29..2c15739 100644 --- a/server/objects/User.ts +++ b/server/objects/User.ts @@ -0,0 +1,138 @@ +import { Database } from "./Database"; +import { LatLng } from "./LatLng"; +import { RankingModes } from "../enums/RankingModes"; +const StatusUpdate = require("./Packets/StatusUpdate.js"); + +const rankingModes = [ + "pp_raw", + "ranked_score", + "avg_accuracy" +]; + +export class User { + private static readonly EMPTY_BUFFER = Buffer.alloc(0); + + public id:number; + public username:string; + public uuid:string; + public readonly connectTime:number = Date.now(); + public timeoutTime:number = Date.now() + 30000; + public queue:Buffer = User.EMPTY_BUFFER; + + // Binato data + public rankingMode:RankingModes = RankingModes.PP; + + // osu! data + public playMode:number = 0; + public countryID:number = 0; + //public spectators:Array; // TODO: Figure out if this was ever needed + public spectating:number = -1; + public location:LatLng = new LatLng(0, 0); + public joinedChannels:Array = new Array(); + + // Presence data + public actionID:number = 0; + public actionText:string = ""; + public actionMods:number = 0; + public beatmapChecksum:string = ""; + public beatmapID:number = 0; + public currentMods:number = 0; + + // Cached db data + public rankedScore:number = 0; + public accuracy:number = 0; + public playCount:number = 0; + public totalScore:number = 0; + public rank:number = 0; + public pp:number = 0; + + // Multiplayer data + public currentMatch = null; + public matchSlotId:number = -1; + public inMatch:boolean = false; + + // Tournament client flag + public isTourneyUser:boolean = false; + + public dbConnection:Database; + + public constructor(id:number, username:string, uuid:string, dbConnection:Database) { + this.id = id; + this.username = username; + this.uuid = uuid; + + this.dbConnection = dbConnection; + } + + // Concats new actions to the user's queue + public addActionToQueue(newData:Buffer) { + this.queue = Buffer.concat([this.queue, newData], this.queue.length + newData.length); + } + + clearQueue() { + this.queue = User.EMPTY_BUFFER; + } + + // Updates the user's current action + updatePresence(action:any) : void { + this.actionID = action.status; + this.actionText = action.statusText; + this.beatmapChecksum = action.beatmapChecksum; + this.currentMods = action.currentMods; + this.actionMods = action.currentMods; + if (action.playMode != this.playMode) { + this.updateUserInfo(true); + this.playMode = action.playMode; + } + this.beatmapID = action.beatmapId; + } + + // Gets the user's score information from the database and caches it + async updateUserInfo(forceUpdate:boolean = false) : Promise { + const userScoreDB:any = await this.dbConnection.query("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]); + const mappedRankingMode = rankingModes[this.rankingMode]; + const userRankDB:any = await this.dbConnection.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]); + + if (userScoreDB == null || userRankDB == null) throw "fuck"; + + // Handle "if we should update" checks for each rankingMode + let userScoreUpdate = false; + switch (this.rankingMode) { + case RankingModes.PP: + if (this.pp != userScoreDB.pp_raw) + userScoreUpdate = true; + break; + + case RankingModes.RANKED_SCORE: + if (this.rankedScore != userScoreDB.ranked_score) + userScoreUpdate = true; + break; + + case RankingModes.AVG_ACCURACY: + if (this.accuracy != userScoreDB.avg_accuracy) + userScoreUpdate = true; + break; + } + + this.rankedScore = userScoreDB.ranked_score; + this.totalScore = userScoreDB.total_score; + this.accuracy = userScoreDB.avg_accuracy; + this.playCount = userScoreDB.playcount; + + // Fetch rank + for (let i = 0; i < userRankDB.length; i++) { + if (userRankDB[i]["user_id"] == this.id) { + this.rank = i + 1; + break; + } + } + + // Set PP to none if ranking mode is not PP + if (this.rankingMode == 0) this.pp = userScoreDB.pp_raw; + else this.pp = 0; + + if (userScoreUpdate || forceUpdate) { + StatusUpdate(this, this.id); + } + } +} \ No newline at end of file diff --git a/server/objects/UserArray.ts b/server/objects/UserArray.ts new file mode 100644 index 0000000..702d3b3 --- /dev/null +++ b/server/objects/UserArray.ts @@ -0,0 +1,26 @@ +import { FunkyArray } from "./FunkyArray"; +import { User } from "./User"; + +export class UserArray extends FunkyArray { + public getById(id:number) : User | undefined { + for (let user of this.getIterableItems()) { + if (user.id == id) + return user; + } + + return undefined; + } + + public getByUsername(username:string) : User | undefined { + for (let user of this.getIterableItems()) { + if (user.username === username) + return user; + } + + return undefined; + } + + public getByToken(token:string) : User | undefined { + return this.getByKey(token); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d8da27b..1e7c369 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "module": "commonjs", "target": "es6", "esModuleInterop": true, + "resolveJsonModule": true, "rootDir": "./", "outDir": "./build", "strict": true diff --git a/web/chatPageTemplate.html b/web/chatPageTemplate.html new file mode 100644 index 0000000..f99a04e --- /dev/null +++ b/web/chatPageTemplate.html @@ -0,0 +1,39 @@ + + + + + + +
+ |content| +
+ + \ No newline at end of file diff --git a/web/serverPage.html b/web/serverPage.html new file mode 100644 index 0000000..5e23aba --- /dev/null +++ b/web/serverPage.html @@ -0,0 +1,19 @@ + + + + Binato + + +
+ 
+  .  o ..
+  o . o o.o
+       ...oo
+          __[]__           Binato
+       __|_o_o_o\__        A custom osu!Bancho
+       \""""""""""/
+        \. ..  . /         Website | Github
+  ^^^^^^^^^^^^^^^^^^^^
+		
+ + \ No newline at end of file