Kickstart the big db rewrite

This commit is contained in:
Holly Stubbs 2023-10-04 15:10:38 +01:00
parent 686e6001b2
commit 4b90031294
15 changed files with 140 additions and 47 deletions

View file

@ -12,8 +12,8 @@ import SpectatorManager from "./SpectatorManager";
import osu from "../osuTyping"; import osu from "../osuTyping";
const shared:Shared = new Shared(); const shared:Shared = new Shared();
shared.database.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL"); shared.database.execute("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL");
shared.database.query("UPDATE osu_info SET value = 0 WHERE name = 'online_now'"); shared.database.execute("UPDATE osu_info SET value = 0 WHERE name = 'online_now'");
// Server Setup // Server Setup
const spectatorManager:SpectatorManager = new SpectatorManager(shared); const spectatorManager:SpectatorManager = new SpectatorManager(shared);
@ -90,7 +90,7 @@ export default async function HandleRequest(req:IncomingMessage, res:ServerRespo
// Client doesn't have a token yet, let's auth them! // Client doesn't have a token yet, let's auth them!
await LoginProcess(req, res, packet, shared); await LoginProcess(req, res, packet, shared);
shared.database.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [shared.users.getLength() - 1]); shared.database.execute("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [shared.users.getLength() - 1]);
} else { } else {
let responseData = Buffer.allocUnsafe(0); let responseData = Buffer.allocUnsafe(0);
@ -240,11 +240,11 @@ export default async function HandleRequest(req:IncomingMessage, res:ServerRespo
break; break;
case Packets.Client_FriendAdd: case Packets.Client_FriendAdd:
AddFriend(user, packet.data); await AddFriend(user, packet.data);
break; break;
case Packets.Client_FriendRemove: case Packets.Client_FriendRemove:
RemoveFriend(user, packet.data); await RemoveFriend(user, packet.data);
break; break;
case Packets.Client_UserStatsRequest: case Packets.Client_UserStatsRequest:

View file

@ -37,7 +37,7 @@ enum LoginResult {
function TestLogin(loginInfo:LoginInfo, shared:Shared) { function TestLogin(loginInfo:LoginInfo, shared:Shared) {
return new Promise<LoginResult>(async (resolve, reject) => { return new Promise<LoginResult>(async (resolve, reject) => {
const userDBData:UserInfo = await shared.database.query("SELECT * FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]); const userDBData = await shared.userInfoRepository.getByUsername(loginInfo.username);
// Make sure a user was found in the database // Make sure a user was found in the database
if (userDBData == null) return resolve(LoginResult.INCORRECT); if (userDBData == null) return resolve(LoginResult.INCORRECT);
@ -123,7 +123,10 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon
} }
// Get information about the user from the database // Get information about the user from the database
const userId:number = (await shared.database.query("SELECT id FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username])).id; const userInfo = await shared.userInfoRepository.getByUsername(loginInfo.username);
if (userInfo == null) {
return;
}
// Create a token for the client // Create a token for the client
const newClientToken:string = await generateSession(); const newClientToken:string = await generateSession();
@ -136,7 +139,7 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon
} }
// Retreive the newly created user // Retreive the newly created user
newUser = shared.users.add(newClientToken, new User(userId, loginInfo.username, newClientToken, shared)); newUser = shared.users.add(newClientToken, new User(userInfo.id, loginInfo.username, newClientToken, shared));
// Set tourney client flag // Set tourney client flag
newUser.isTourneyUser = isTourneyClient; newUser.isTourneyUser = isTourneyClient;
newUser.location = userLocation; newUser.location = userLocation;

View file

@ -1,4 +1,5 @@
export enum Permissions { export enum Permissions {
None = 0,
BAT = 2, BAT = 2,
Supporter = 4, Supporter = 4,
Friend = 8, Friend = 8,

View file

@ -1,5 +1,6 @@
import { ConsoleHelper } from "../../ConsoleHelper"; import { ConsoleHelper } from "../../ConsoleHelper";
import { createPool, Pool } from "mysql2"; import { createPool, Pool, RowDataPacket } from "mysql2";
import { DBInDataType } from "../types/DBTypes";
export default class Database { export default class Database {
private connectionPool:Pool; private connectionPool:Pool;
@ -20,50 +21,70 @@ export default class Database {
ConsoleHelper.printInfo(`Connected DB connection pool. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`); ConsoleHelper.printInfo(`Connected DB connection pool. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`);
} }
public query(query = "", data?:Array<any>) { public execute(query:string, data?:Array<DBInDataType>) {
const limited = query.includes("LIMIT 1"); return new Promise<boolean>((resolve, reject) => {
return new Promise<any>((resolve, reject) => {
this.connectionPool.getConnection((err, connection) => { this.connectionPool.getConnection((err, connection) => {
if (err) { if (err) {
reject(err); return reject(err);
try {
connection.release();
} catch (e) {
ConsoleHelper.printError("Failed to release mysql connection\n" + err);
} }
if (data == null) {
connection.execute(query, data, (err, result) => {
if (err) {
connection.release();
return reject(err);
}
resolve(result !== undefined);
});
} else {
}
});
});
}
public query(query:string, data?:Array<DBInDataType>) {
return new Promise<RowDataPacket[]>((resolve, reject) => {
this.connectionPool.getConnection((err, connection) => {
if (err) {
return reject(err);
} else { } else {
// Use old query // Use old query
if (data == null) { if (data == null) {
connection.query(query, (err, data) => { connection.query<RowDataPacket[]>(query, (err, rows) => {
connection.release();
if (err) { if (err) {
reject(err); return reject(err);
connection.release();
} else {
dataReceived(resolve, data, limited);
connection.release();
} }
resolve(rows);
connection.release();
}); });
} }
// Use new prepared statements w/ placeholders // Use new prepared statements w/ placeholders
else { else {
connection.execute(query, data, (err, data) => { connection.execute<RowDataPacket[]>(query, data, (err, rows) => {
connection.release();
if (err) { if (err) {
reject(err); return reject(err);
connection.release();
} else {
dataReceived(resolve, data, limited);
connection.release();
}
});
}
}
});
});
}
} }
function dataReceived(resolveCallback:(value:unknown) => void, data:any, limited:boolean = false) : void { resolve(rows);
if (limited) resolveCallback(data[0]); connection.release();
else resolveCallback(data); });
}
}
});
});
}
public async querySingle(query:string, data?:Array<DBInDataType>) {
const dbData = await this.query(query, data);
if (dbData != null && dbData.length > 0) {
return dbData[0];
}
return null;
}
} }

View file

@ -29,7 +29,7 @@ export default class FunkyArray<T> {
} }
public regenerateIterableArray() : void { public regenerateIterableArray() : void {
this.iterableArray = new Array(); this.iterableArray = new Array<T>();
for (const itemKey of this.itemKeys) { for (const itemKey of this.itemKeys) {
this.iterableArray.push(this.items[itemKey]); this.iterableArray.push(this.items[itemKey]);
} }
@ -64,7 +64,7 @@ export default class FunkyArray<T> {
return this.itemKeys; return this.itemKeys;
} }
public getItems() : any { public getItems() : { [id: string]: T } {
return this.items; return this.items;
} }

View file

@ -242,7 +242,7 @@ export default class Match {
} }
queryData.push(this.matchId); queryData.push(this.matchId);
await this.shared.database.query( await this.shared.database.execute(
`UPDATE mp_matches SET ${gameNameChanged ? `name = ?${gameSeedChanged ? ", " : ""}` : ""}${gameSeedChanged ? `seed = ?` : ""} WHERE id = ?`, `UPDATE mp_matches SET ${gameNameChanged ? `name = ?${gameSeedChanged ? ", " : ""}` : ""}${gameSeedChanged ? `seed = ?` : ""} WHERE id = ?`,
queryData queryData
); );
@ -632,7 +632,7 @@ export default class Match {
slot.status = SlotStatus.NotReady; slot.status = SlotStatus.NotReady;
} }
await this.shared.database.query("INSERT INTO mp_match_rounds (id, match_id, round_id, round_mode, match_type, round_scoring_type, round_team_type, round_mods, beatmap_md5, freemod, player0, player1, player2, player3, player4, player5, player6, player7, player8, player9, player10, player11, player12, player13, player14, player15) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", queryData); await this.shared.database.execute("INSERT INTO mp_match_rounds (id, match_id, round_id, round_mode, match_type, round_scoring_type, round_team_type, round_mods, beatmap_md5, freemod, player0, player1, player2, player3, player4, player5, player6, player7, player8, player9, player10, player11, player12, player13, player14, player15) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", queryData);
osuPacketWriter.MatchComplete(); osuPacketWriter.MatchComplete();

View file

@ -10,6 +10,7 @@ import User from "./User";
import LatLng from "./LatLng"; import LatLng from "./LatLng";
import Bot from "../Bot"; import Bot from "../Bot";
import { ConsoleHelper } from "../../ConsoleHelper"; import { ConsoleHelper } from "../../ConsoleHelper";
import UserInfoRepository from "../repos/UserInfoRepository";
export default class Shared { export default class Shared {
public readonly chatManager:ChatManager; public readonly chatManager:ChatManager;
@ -21,6 +22,8 @@ export default class Shared {
public readonly users:UserArray; public readonly users:UserArray;
public readonly bot:Bot; public readonly bot:Bot;
public readonly userInfoRepository:UserInfoRepository;
public constructor() { public constructor() {
if (!existsSync("./config.json")) { if (!existsSync("./config.json")) {
ConsoleHelper.printError("Config file missing!"); ConsoleHelper.printError("Config file missing!");
@ -46,5 +49,8 @@ export default class Shared {
this.multiplayerManager = new MultiplayerManager(this); this.multiplayerManager = new MultiplayerManager(this);
this.privateChatManager = new PrivateChatManager(this); this.privateChatManager = new PrivateChatManager(this);
// DB Repos
this.userInfoRepository = new UserInfoRepository(this);
} }
} }

View file

@ -98,7 +98,7 @@ export default class User {
// Gets the user's score information from the database and caches it // Gets the user's score information from the database and caches it
async updateUserInfo(forceUpdate:boolean = false) { async updateUserInfo(forceUpdate:boolean = false) {
const userScoreDB = await this.shared.database.query("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]); const userScoreDB = await this.shared.database.querySingle("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]);
const mappedRankingMode = rankingModes[this.rankingMode]; const mappedRankingMode = rankingModes[this.rankingMode];
const userRankDB = await this.shared.database.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]); const userRankDB = await this.shared.database.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]);

View file

@ -1,3 +1,5 @@
import { Permissions } from "../../enums/Permissions";
export default class UserInfo { export default class UserInfo {
id:number = Number.MIN_VALUE; id:number = Number.MIN_VALUE;
username:string = ""; username:string = "";
@ -10,7 +12,7 @@ export default class UserInfo {
last_login_date:Date = new Date(0); last_login_date:Date = new Date(0);
last_played_mode:number = Number.MIN_VALUE; last_played_mode:number = Number.MIN_VALUE;
online_now:boolean = false; online_now:boolean = false;
tags:number = Number.MIN_VALUE; tags:Permissions = Permissions.None;
supporter:boolean = false; supporter:boolean = false;
web_session:string = ""; web_session:string = "";
verification_needed:boolean = false; verification_needed:boolean = false;

View file

@ -1,7 +1,7 @@
import User from "../objects/User"; import User from "../objects/User";
export default function AddFriend(user:User, friendId:number) { export default async function AddFriend(user:User, friendId:number) {
user.shared.database.query("INSERT INTO friends (user, friendsWith) VALUES (?, ?)", [ await user.shared.database.execute("INSERT INTO friends (user, friendsWith) VALUES (?, ?)", [
user.id, friendId user.id, friendId
]); ]);
} }

View file

@ -13,7 +13,7 @@ export default async function Logout(user:User) {
// Remove user from user list // Remove user from user list
user.shared.users.remove(user.uuid); user.shared.users.remove(user.uuid);
await user.shared.database.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [user.shared.users.getLength() - 1]); await user.shared.database.execute("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [user.shared.users.getLength() - 1]);
ConsoleHelper.printBancho(`User logged out, took ${Date.now() - logoutStartTime}ms. [User: ${user.username}]`); ConsoleHelper.printBancho(`User logged out, took ${Date.now() - logoutStartTime}ms. [User: ${user.username}]`);
} }

View file

@ -1,7 +1,7 @@
import User from "../objects/User"; import User from "../objects/User";
export default function RemoveFriend(user:User, friendId:number) { export default async function RemoveFriend(user:User, friendId:number) {
user.shared.database.query("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [ await user.shared.database.execute("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [
user.id, friendId user.id, friendId
]); ]);
} }

View file

@ -0,0 +1,59 @@
import { RowDataPacket } from "mysql2";
import Database from "../objects/Database";
import Shared from "../objects/Shared";
import UserInfo from "../objects/database/UserInfo";
export default class UserInfoRepository {
private database:Database;
public constructor(shared:Shared) {
this.database = shared.database;
}
public async getById(id:number) {
const query = await this.database.query("SELECT * FROM users_info WHERE id = ? AND is_deleted = 0 LIMIT 1", [id]);
if (query != null) {
const userInfo = new UserInfo();
populateUserInfoFromRowDataPacket(userInfo, query[0]);
return userInfo;
}
return null;
}
public async getByUsername(username:string) {
const query = await this.database.query("SELECT * FROM users_info WHERE username = ? AND is_deleted = 0 LIMIT 1", [username]);
if (query != null) {
const userInfo = new UserInfo();
populateUserInfoFromRowDataPacket(userInfo, query[0]);
return userInfo;
}
return null;
}
}
function populateUserInfoFromRowDataPacket(userInfo:UserInfo, rowDataPacket:RowDataPacket) {
userInfo.id = rowDataPacket["id"];
userInfo.username = rowDataPacket["username"];
userInfo.username_safe = rowDataPacket["username_safe"];
userInfo.password_hash = rowDataPacket["password_hash"];
userInfo.password_salt = rowDataPacket["password_salt"];
userInfo.email = rowDataPacket["email"];
userInfo.country = rowDataPacket["country"];
userInfo.reg_date = rowDataPacket["reg_date"];
userInfo.last_login_date = rowDataPacket["last_login_date"];
userInfo.last_played_mode = rowDataPacket["last_played_mode"];
userInfo.online_now = rowDataPacket["online_now"];
userInfo.tags = rowDataPacket["tags"];
userInfo.supporter = rowDataPacket["supporter"];
userInfo.web_session = rowDataPacket["web_session"];
userInfo.verification_needed = rowDataPacket["verification_needed"];
userInfo.password_change_required = rowDataPacket["password_change_required"];
userInfo.has_old_password = rowDataPacket["has_old_password"];
userInfo.password_reset_key = rowDataPacket["password_reset_key"];
userInfo.away_message = rowDataPacket["away_message"];
userInfo.last_modified_time = rowDataPacket["last_modified_time"];
userInfo.is_deleted = rowDataPacket["is_deleted"];
}

1
server/types/DBTypes.ts Normal file
View file

@ -0,0 +1 @@
export type DBInDataType = string | number | null | undefined;

View file

@ -53,7 +53,7 @@ import { Registry, collectDefaultMetrics } from "prom-client";
import { RedisClientType, createClient } from "redis"; import { RedisClientType, createClient } from "redis";
import { readFileSync, existsSync } from "fs"; import { readFileSync, existsSync } from "fs";
import { randomBytes, pbkdf2 } from "crypto"; import { randomBytes, pbkdf2 } from "crypto";
import { createPool, Pool } from "mysql2"; import { createPool, Pool, RowDataPacket } from "mysql2";
import * as dyetty from "dyetty"; import * as dyetty from "dyetty";
import fetch from "node-fetch"; import fetch from "node-fetch";
import http from "http";`); import http from "http";`);