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