diff --git a/server/BanchoServer.ts b/server/BanchoServer.ts index 3f570eb..bc6c0c6 100644 --- a/server/BanchoServer.ts +++ b/server/BanchoServer.ts @@ -12,8 +12,8 @@ import SpectatorManager from "./SpectatorManager"; import osu from "../osuTyping"; const shared:Shared = new Shared(); -shared.database.query("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 mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL"); +shared.database.execute("UPDATE osu_info SET value = 0 WHERE name = 'online_now'"); // Server Setup 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! 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 { let responseData = Buffer.allocUnsafe(0); @@ -240,11 +240,11 @@ export default async function HandleRequest(req:IncomingMessage, res:ServerRespo break; case Packets.Client_FriendAdd: - AddFriend(user, packet.data); + await AddFriend(user, packet.data); break; case Packets.Client_FriendRemove: - RemoveFriend(user, packet.data); + await RemoveFriend(user, packet.data); break; case Packets.Client_UserStatsRequest: diff --git a/server/LoginProcess.ts b/server/LoginProcess.ts index 053f711..6a2c4d7 100644 --- a/server/LoginProcess.ts +++ b/server/LoginProcess.ts @@ -37,7 +37,7 @@ enum LoginResult { function TestLogin(loginInfo:LoginInfo, shared:Shared) { return new Promise(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 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 - 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 const newClientToken:string = await generateSession(); @@ -136,7 +139,7 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon } // 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 newUser.isTourneyUser = isTourneyClient; newUser.location = userLocation; diff --git a/server/enums/Permissions.ts b/server/enums/Permissions.ts index fc1032d..fb06ff0 100644 --- a/server/enums/Permissions.ts +++ b/server/enums/Permissions.ts @@ -1,4 +1,5 @@ export enum Permissions { + None = 0, BAT = 2, Supporter = 4, Friend = 8, diff --git a/server/objects/Database.ts b/server/objects/Database.ts index 3a6f7dc..4cb77aa 100644 --- a/server/objects/Database.ts +++ b/server/objects/Database.ts @@ -1,5 +1,6 @@ import { ConsoleHelper } from "../../ConsoleHelper"; -import { createPool, Pool } from "mysql2"; +import { createPool, Pool, RowDataPacket } from "mysql2"; +import { DBInDataType } from "../types/DBTypes"; export default class Database { private connectionPool:Pool; @@ -20,50 +21,70 @@ export default class Database { ConsoleHelper.printInfo(`Connected DB connection pool. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`); } - public query(query = "", data?:Array) { - const limited = query.includes("LIMIT 1"); - - return new Promise((resolve, reject) => { + public execute(query:string, data?:Array) { + 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); - } + return reject(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) { + return new Promise((resolve, reject) => { + this.connectionPool.getConnection((err, connection) => { + if (err) { + return reject(err); } else { // Use old query if (data == null) { - connection.query(query, (err, data) => { + connection.query(query, (err, rows) => { + connection.release(); if (err) { - reject(err); - connection.release(); - } else { - dataReceived(resolve, data, limited); - connection.release(); + return reject(err); } + + resolve(rows); + connection.release(); }); } // Use new prepared statements w/ placeholders else { - connection.execute(query, data, (err, data) => { + connection.execute(query, data, (err, rows) => { + connection.release(); if (err) { - reject(err); - connection.release(); - } else { - dataReceived(resolve, data, limited); - connection.release(); + return reject(err); } + + resolve(rows); + connection.release(); }); } } }); }); } -} -function dataReceived(resolveCallback:(value:unknown) => void, data:any, limited:boolean = false) : void { - if (limited) resolveCallback(data[0]); - else resolveCallback(data); + public async querySingle(query:string, data?:Array) { + const dbData = await this.query(query, data); + if (dbData != null && dbData.length > 0) { + return dbData[0]; + } + + return null; + } } \ No newline at end of file diff --git a/server/objects/FunkyArray.ts b/server/objects/FunkyArray.ts index 83623ca..ce4cc0a 100644 --- a/server/objects/FunkyArray.ts +++ b/server/objects/FunkyArray.ts @@ -29,7 +29,7 @@ export default class FunkyArray { } public regenerateIterableArray() : void { - this.iterableArray = new Array(); + this.iterableArray = new Array(); for (const itemKey of this.itemKeys) { this.iterableArray.push(this.items[itemKey]); } @@ -64,7 +64,7 @@ export default class FunkyArray { return this.itemKeys; } - public getItems() : any { + public getItems() : { [id: string]: T } { return this.items; } diff --git a/server/objects/Match.ts b/server/objects/Match.ts index 9d343b9..0e35290 100644 --- a/server/objects/Match.ts +++ b/server/objects/Match.ts @@ -242,7 +242,7 @@ export default class Match { } 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 = ?`, queryData ); @@ -632,7 +632,7 @@ export default class Match { 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(); diff --git a/server/objects/Shared.ts b/server/objects/Shared.ts index eff7aba..0626890 100644 --- a/server/objects/Shared.ts +++ b/server/objects/Shared.ts @@ -10,6 +10,7 @@ import User from "./User"; import LatLng from "./LatLng"; import Bot from "../Bot"; import { ConsoleHelper } from "../../ConsoleHelper"; +import UserInfoRepository from "../repos/UserInfoRepository"; export default class Shared { public readonly chatManager:ChatManager; @@ -21,6 +22,8 @@ export default class Shared { public readonly users:UserArray; public readonly bot:Bot; + public readonly userInfoRepository:UserInfoRepository; + public constructor() { if (!existsSync("./config.json")) { ConsoleHelper.printError("Config file missing!"); @@ -46,5 +49,8 @@ export default class Shared { this.multiplayerManager = new MultiplayerManager(this); this.privateChatManager = new PrivateChatManager(this); + + // DB Repos + this.userInfoRepository = new UserInfoRepository(this); } } \ No newline at end of file diff --git a/server/objects/User.ts b/server/objects/User.ts index 9e549c1..cb12a8f 100644 --- a/server/objects/User.ts +++ b/server/objects/User.ts @@ -98,7 +98,7 @@ export default class User { // Gets the user's score information from the database and caches it 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 userRankDB = await this.shared.database.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]); diff --git a/server/objects/database/UserInfo.ts b/server/objects/database/UserInfo.ts index ce3d5d3..a67afc7 100644 --- a/server/objects/database/UserInfo.ts +++ b/server/objects/database/UserInfo.ts @@ -1,3 +1,5 @@ +import { Permissions } from "../../enums/Permissions"; + export default class UserInfo { id:number = Number.MIN_VALUE; username:string = ""; @@ -10,7 +12,7 @@ export default class UserInfo { last_login_date:Date = new Date(0); last_played_mode:number = Number.MIN_VALUE; online_now:boolean = false; - tags:number = Number.MIN_VALUE; + tags:Permissions = Permissions.None; supporter:boolean = false; web_session:string = ""; verification_needed:boolean = false; diff --git a/server/packets/AddFriend.ts b/server/packets/AddFriend.ts index 97b9787..4dff492 100644 --- a/server/packets/AddFriend.ts +++ b/server/packets/AddFriend.ts @@ -1,7 +1,7 @@ import User from "../objects/User"; -export default function AddFriend(user:User, friendId:number) { - user.shared.database.query("INSERT INTO friends (user, friendsWith) VALUES (?, ?)", [ +export default async function AddFriend(user:User, friendId:number) { + await user.shared.database.execute("INSERT INTO friends (user, friendsWith) VALUES (?, ?)", [ user.id, friendId ]); } \ No newline at end of file diff --git a/server/packets/Logout.ts b/server/packets/Logout.ts index f43a831..81ba065 100644 --- a/server/packets/Logout.ts +++ b/server/packets/Logout.ts @@ -13,7 +13,7 @@ export default async function Logout(user:User) { // Remove user from user list 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}]`); } \ No newline at end of file diff --git a/server/packets/RemoveFriend.ts b/server/packets/RemoveFriend.ts index aa36f9a..7b7c32f 100644 --- a/server/packets/RemoveFriend.ts +++ b/server/packets/RemoveFriend.ts @@ -1,7 +1,7 @@ import User from "../objects/User"; -export default function RemoveFriend(user:User, friendId:number) { - user.shared.database.query("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [ +export default async function RemoveFriend(user:User, friendId:number) { + await user.shared.database.execute("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [ user.id, friendId ]); } \ No newline at end of file diff --git a/server/repos/UserInfoRepository.ts b/server/repos/UserInfoRepository.ts new file mode 100644 index 0000000..0021db5 --- /dev/null +++ b/server/repos/UserInfoRepository.ts @@ -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"]; +} \ No newline at end of file diff --git a/server/types/DBTypes.ts b/server/types/DBTypes.ts new file mode 100644 index 0000000..c97d22d --- /dev/null +++ b/server/types/DBTypes.ts @@ -0,0 +1 @@ +export type DBInDataType = string | number | null | undefined; \ No newline at end of file diff --git a/tooling/fileSmasher.ts b/tooling/fileSmasher.ts index 406681f..2d49b83 100644 --- a/tooling/fileSmasher.ts +++ b/tooling/fileSmasher.ts @@ -53,7 +53,7 @@ import { Registry, collectDefaultMetrics } from "prom-client"; import { RedisClientType, createClient } from "redis"; import { readFileSync, existsSync } from "fs"; import { randomBytes, pbkdf2 } from "crypto"; -import { createPool, Pool } from "mysql2"; +import { createPool, Pool, RowDataPacket } from "mysql2"; import * as dyetty from "dyetty"; import fetch from "node-fetch"; import http from "http";`);