kopasdkopsdaokp

This commit is contained in:
Holly Stubbs 2022-11-16 15:25:46 +00:00
parent 4ebf9ee0e6
commit 53a12461ce
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
16 changed files with 694 additions and 114 deletions

View file

@ -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 += `<div class="line line${flip ? 1 : 0}">${global.chatHistory[i] == null ? "<hidden>blank</hidden>" : global.chatHistory[i]}</div>`
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!<br>Binato only accepts POST requests on Bancho subdomains.<hr>Binato");
break;

165
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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;

34
server/ChatHistory.ts Normal file
View file

@ -0,0 +1,34 @@
import { readFileSync } from "fs";
export abstract class ChatHistory {
private static _history:Array<string> = new Array<string>();
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 += `<div class="line line${flip ? 1 : 0}">${this._history[i] == null ? "<hidden>blank</hidden>" : this._history[i]}</div>`
flip = !flip;
}
if (this._hasChanged) {
this._lastGeneratedPage = this.PAGE_TEMPLATE.toString().replace("|content|", lines);
this._hasChanged = false;
}
return this._lastGeneratedPage;
}
}

8
server/Util.ts Normal file
View file

@ -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);
}

View file

@ -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,
}

View file

@ -0,0 +1,5 @@
export enum RankingModes {
PP,
RANKED_SCORE,
AVG_ACCURACY
};

View file

@ -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<any>) {
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);
}

View file

@ -0,0 +1,74 @@
export class FunkyArray<T> {
private items:any = {};
private itemKeys:Array<string> = Object.keys(this.items);
private iterableArray:Array<T> = new Array<T>();
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<string> {
return this.itemKeys;
}
public getItems() : any {
return this.items;
}
public getIterableItems() : Array<T> {
return this.iterableArray;
}
}

9
server/objects/LatLng.ts Normal file
View file

@ -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;
}
}

View file

@ -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<string> = new Array<string>();
// 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<void> {
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);
}
}
}

View file

@ -0,0 +1,26 @@
import { FunkyArray } from "./FunkyArray";
import { User } from "./User";
export class UserArray extends FunkyArray<User> {
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);
}
}

View file

@ -3,6 +3,7 @@
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"resolveJsonModule": true,
"rootDir": "./",
"outDir": "./build",
"strict": true

39
web/chatPageTemplate.html Normal file
View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 0;
}
.container {
width: 482px;
height: 165px;
}
hidden {
visibility: hidden;
}
.line {
padding: 2px;
font-size: 8pt;
font-family: sans-serif;
}
.line0 {
background-color: #edebfa;
}
.line1 {
background-color: #e3e1fa;
}
</style>
</head>
<body>
<div class="container">
|content|
</div>
</body>
</html>

19
web/serverPage.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Binato</title>
</head>
<body>
<pre style="border-style:double;border-width:4px;width:376px;">
. o ..
o . o o.o
...oo
__[]__ <b>Binato</b>
__|_o_o_o\__ A custom osu!Bancho
\""""""""""/
\. .. . / <a href="https://binato.eusv.ml">Website</a> | <a href="https://github.com/tgpethan/Binato">Github</a>
^^^^^^^^^^^^^^^^^^^^
</pre>
</body>
</html>