This commit is contained in:
Holly Stubbs 2023-08-20 13:03:01 +01:00
parent 1a871e4c35
commit 734cebb19e
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
48 changed files with 2881 additions and 3706 deletions

View file

@ -12,12 +12,10 @@ import { ChatHistory } from "./server/ChatHistory";
import { Config } from "./server/interfaces/Config"; import { Config } from "./server/interfaces/Config";
import compression from "compression"; import compression from "compression";
import express from "express"; import express from "express";
import { HandleRequest, GetSharedContent } from "./server/BanchoServer"; import { HandleRequest } from "./server/BanchoServer";
import { SharedContent } from "./server/interfaces/SharedContent"; import { Shared } from "./server/objects/Shared";
import { Registry, collectDefaultMetrics } from "prom-client"; import { Registry, collectDefaultMetrics } from "prom-client";
const config:Config = JSON.parse(readFileSync(__dirname + "/config.json").toString()) as Config; const config:Config = JSON.parse(readFileSync(__dirname + "/config.json").toString()) as Config;
// Pull out shared data from BanchoServer
const sharedContent:SharedContent = GetSharedContent();
const binatoApp:express.Application = express(); const binatoApp:express.Application = express();
@ -55,6 +53,7 @@ binatoApp.use((req, res) => {
if (req.url == "/" || req.url == "/index.html" || req.url == "/index") { if (req.url == "/" || req.url == "/index.html" || req.url == "/index") {
res.send(INDEX_PAGE); res.send(INDEX_PAGE);
} else if (req.url == "/chat") { } else if (req.url == "/chat") {
// I don't think this works??
res.send(ChatHistory.GenerateForWeb()); res.send(ChatHistory.GenerateForWeb());
} }
break; break;

View file

@ -1,4 +1,4 @@
import chalk from "chalk"; import * as dyetty from "dyetty";
enum LogType { enum LogType {
INFO, INFO,
@ -7,14 +7,14 @@ enum LogType {
}; };
const LogTags = { const LogTags = {
INFO: chalk.bgGreen(chalk.black(" INFO ")), INFO: dyetty.bgGreen(dyetty.black(" INFO ")),
BANCHO: chalk.bgMagenta(chalk.black(" BANCHO ")), BANCHO: dyetty.bgMagenta(dyetty.black(" BANCHO ")),
WEBREQ: chalk.bgGreen(chalk.black(" WEBREQ ")), WEBREQ: dyetty.bgGreen(dyetty.black(" WEBREQ ")),
CHAT: chalk.bgCyan(chalk.black(" CHAT ")), CHAT: dyetty.bgCyan(dyetty.black(" CHAT ")),
WARN: chalk.bgYellow(chalk.black(" WARN ")), WARN: dyetty.bgYellow(dyetty.black(" WARN ")),
ERROR: chalk.bgRed(" ERRR "), ERROR: dyetty.bgRed(" ERRR "),
REDIS: chalk.bgRed(chalk.white(" bREDIS ")), REDIS: dyetty.bgRed(dyetty.white(" bREDIS ")),
STREAM: chalk.bgBlue(chalk.black(" STREAM ")) STREAM: dyetty.bgBlue(dyetty.black(" STREAM "))
} as const; } as const;
function correctValue(i:number) : string { function correctValue(i:number) : string {
@ -24,7 +24,7 @@ function correctValue(i:number) : string {
function getTime() : string { function getTime() : string {
const time = new Date(); const time = new Date();
return chalk.green(`[${correctValue(time.getHours())}:${correctValue(time.getMinutes())}:${correctValue(time.getSeconds())}]`); return dyetty.green(`[${correctValue(time.getHours())}:${correctValue(time.getMinutes())}:${correctValue(time.getSeconds())}]`);
} }
function log(tag:string, log:string, logType:LogType = LogType.INFO) : void { function log(tag:string, log:string, logType:LogType = LogType.INFO) : void {

View file

@ -1,3 +1,4 @@
export abstract class Constants { export abstract class Constants {
public static readonly DEBUG = false; public static readonly DEBUG = false;
public static readonly PROTOCOL_VERSION = 19;
} }

18
osuTyping.ts Normal file
View file

@ -0,0 +1,18 @@
import { OsuPacketWriter } from "./server/interfaces/OsuPacketWriter";
// Dummy file
const nodeOsu = require("osu-packet");
export abstract class osu {
static Bancho = {
Writer: function() : OsuPacketWriter {
return new nodeOsu.Bancho.Writer();
}
};
static Client = {
Reader: function(data:any) : any {
return new nodeOsu.Client.Reader(data);
}
};
}

4531
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,13 @@
"description": "", "description": "",
"main": "Binato.ts", "main": "Binato.ts",
"scripts": { "scripts": {
"dev:updateCheck": "check-outdated",
"dev:run": "nodemon --watch './**/*.ts' Binato.ts", "dev:run": "nodemon --watch './**/*.ts' Binato.ts",
"pack": "webpack", "build": "npm-run-all build:*",
"build": "tsc --build", "build:smash": "ts-node ./tooling/fileSmasher.ts",
"build:build": "tsc --build",
"build:mangle": "ts-node ./tooling/mangle.ts",
"build:cleanup": "ts-node ./tooling/cleanup.ts",
"_clean": "tsc --build --clean" "_clean": "tsc --build --clean"
}, },
"keywords": [], "keywords": [],
@ -14,26 +18,25 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"aes256": "^1.1.0", "aes256": "^1.1.0",
"chalk": "^4.1.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"dyetty": "^1.0.1",
"express": "^4.18.2", "express": "^4.18.2",
"mysql2": "^2.3.3", "mysql2": "^3.6.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.13",
"osu-packet": "^4.1.2", "osu-packet": "^4.1.2",
"prom-client": "^14.1.0", "prom-client": "^14.2.0",
"redis": "^4.5.0" "redis": "^4.6.7"
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/express": "^4.17.14", "@types/express": "^4.17.17",
"@types/node": "^18.11.9", "@types/node": "^20.5.1",
"@types/node-fetch": "^2.6.2", "@types/node-fetch": "^2.6.4",
"nodemon": "^2.0.20", "check-outdated": "^2.12.0",
"ts-loader": "^9.4.1", "nodemon": "^3.0.1",
"npm-run-all": "^4.1.5",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.3", "typescript": "^5.1.6"
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-node-externals": "^3.0.0"
} }
} }

View file

@ -1,58 +1,22 @@
import { Config } from "./interfaces/Config";
import { ConsoleHelper } from "../ConsoleHelper"; import { ConsoleHelper } from "../ConsoleHelper";
import { Channel } from "./objects/Channel"; import { Channel } from "./objects/Channel";
import { ChatManager } from "./ChatManager";
import { Database } from "./objects/Database";
import { DataStreamArray } from "./objects/DataStreamArray";
import { LatLng } from "./objects/LatLng"; import { LatLng } from "./objects/LatLng";
import { LoginProcess } from "./LoginProcess"; import { LoginProcess } from "./LoginProcess";
import { Packets } from "./enums/Packets"; import { Packets } from "./enums/Packets";
import { replaceAll } from "./Util";
import { readFileSync } from "fs";
import { RedisClientType, createClient } from "redis"; import { RedisClientType, createClient } from "redis";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { SpectatorManager } from "./SpectatorManager"; import { SpectatorManager } from "./SpectatorManager";
import { UserArray } from "./objects/UserArray";
import { User } from "./objects/User"; import { User } from "./objects/User";
import { MultiplayerManager } from "./MultiplayerManager"; import { PrivateMessage } from "./packets/PrivateMessage";
import { SharedContent } from "./interfaces/SharedContent"; import { MessageData } from "./interfaces/MessageData";
const config:Config = JSON.parse(readFileSync("./config.json").toString()) as Config; import { Shared } from "./objects/Shared";
// TODO: Port osu-packet to TypeScript
const osu = require("osu-packet");
const sharedContent:any = {}; const shared:Shared = new Shared();
// NOTE: This function should only be used externaly in Binato.ts and in this file. shared.database.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL");
export function GetSharedContent() : SharedContent { shared.database.query("UPDATE osu_info SET value = 0 WHERE name = 'online_now'");
return sharedContent;
}
const DB:Database = sharedContent.database = new Database(config.database.address, config.database.port, config.database.username, config.database.password, config.database.name, async () => { // Server Setup
// Close any unclosed db matches on startup const spectatorManager:SpectatorManager = new SpectatorManager(shared);
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'");
});
// User session storage
const users:UserArray = sharedContent.users = new UserArray();
// Add the bot user
const botUser:User = users.add("bot", new User(3, "SillyBot", "bot", GetSharedContent()));
// Set the bot's position on the map
botUser.location = new LatLng(50, -32);
// DataStream storage
const streams:DataStreamArray = sharedContent.streams = new DataStreamArray();
// ChatManager
const chatManager:ChatManager = sharedContent.chatManager = new ChatManager(GetSharedContent());
chatManager.AddChatChannel("osu", "The main channel", true);
chatManager.AddChatChannel("lobby", "Talk about multiplayer stuff");
chatManager.AddChatChannel("english", "Talk in exclusively English");
chatManager.AddChatChannel("japanese", "Talk in exclusively Japanese");
const multiplayerManager:MultiplayerManager = sharedContent.mutiplayerManager = new MultiplayerManager(GetSharedContent());
const spectatorManager:SpectatorManager = new SpectatorManager(GetSharedContent());
let redisClient:RedisClientType; let redisClient:RedisClientType;
@ -65,10 +29,10 @@ async function subscribeToChannel(channelName:string, callback:(message:string)
ConsoleHelper.printRedis(`Subscribed to ${channelName} channel`); ConsoleHelper.printRedis(`Subscribed to ${channelName} channel`);
} }
if (config.redis.enabled) { if (shared.config.redis.enabled) {
(async () => { (async () => {
redisClient = createClient({ redisClient = createClient({
url: `redis://${replaceAll(config.redis.password, " ", "") == "" ? "" : `${config.redis.password}@`}${config.redis.address}:${config.redis.port}/${config.redis.database}` url: `redis://${shared.config.redis.password.replaceAll(" ", "") == "" ? "" : `${shared.config.redis.password}@`}${shared.config.redis.address}:${shared.config.redis.port}/${shared.config.redis.database}`
}); });
redisClient.on('error', e => ConsoleHelper.printRedis(e)); redisClient.on('error', e => ConsoleHelper.printRedis(e));
@ -79,14 +43,12 @@ if (config.redis.enabled) {
// Score submit update channel // Score submit update channel
subscribeToChannel("binato:update_user_stats", (message) => { subscribeToChannel("binato:update_user_stats", (message) => {
if (typeof(message) === "string") { const user = shared.users.getById(parseInt(message));
const user = users.getById(parseInt(message)); if (user != null) {
if (user != null) { // Update user info
// Update user info user.updateUserInfo(true);
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}`);
}
} }
}); });
})(); })();
@ -98,10 +60,18 @@ import { Logout } from "./packets/Logout";
import { UserPresence } from "./packets/UserPresence"; import { UserPresence } from "./packets/UserPresence";
import { UserStatsRequest } from "./packets/UserStatsRequest"; import { UserStatsRequest } from "./packets/UserStatsRequest";
import { UserPresenceBundle } from "./packets/UserPresenceBundle"; import { UserPresenceBundle } from "./packets/UserPresenceBundle";
import { TourneyMatchSpecialInfo } from "./packets/TourneyMatchSpecialInfo";
import { osu } from "../osuTyping";
import { TourneyMatchJoinChannel } from "./packets/TourneyJoinMatchChannel";
import { TourneyMatchLeaveChannel } from "./packets/TourneyMatchLeaveChannel";
import { AddFriend } from "./packets/AddFriend";
import { RemoveFriend } from "./packets/RemoveFriend";
import { PrivateChannel } from "./objects/PrivateChannel";
import { Constants } from "../Constants";
// User timeout interval // User timeout interval
setInterval(() => { setInterval(() => {
for (let User of users.getIterableItems()) { for (let User of shared.users.getIterableItems()) {
if (User.uuid == "bot") continue; // Ignore the bot if (User.uuid == "bot") continue; // Ignore the bot
// Logout this user, they're clearly gone. // Logout this user, they're clearly gone.
@ -114,29 +84,29 @@ setInterval(() => {
const EMPTY_BUFFER = Buffer.alloc(0); const EMPTY_BUFFER = Buffer.alloc(0);
export async function HandleRequest(req:Request, res:Response, packet:Buffer) { export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
// Remove headers we don't need for Bancho
res.removeHeader('X-Powered-By');
res.removeHeader('Date');
// Get the client's token string and request data // Get the client's token string and request data
const requestTokenString:string | undefined = req.header("osu-token"); const requestTokenString:string | undefined = req.header("osu-token");
// Check if the user is logged in // Check if the user is logged in
if (requestTokenString == null) { if (requestTokenString == null) {
// Only do this if we're absolutely sure that we're connected to the DB // Only do this if we're absolutely sure that we're connected to the DB
if (DB.connected) { if (shared.database.connected) {
// 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, GetSharedContent()); await LoginProcess(req, res, packet, shared);
DB.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [users.getLength() - 1]); shared.database.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [shared.users.getLength() - 1]);
} }
} else { } else {
let responseData:Buffer | string = EMPTY_BUFFER; let responseData:Buffer | string = EMPTY_BUFFER;
// Remove headers we don't need for Bancho
res.removeHeader('X-Powered-By');
res.removeHeader('Date'); // This is not spec compilant
// Client has a token, let's see what they want. // Client has a token, let's see what they want.
try { try {
// Get the current user // Get the current user
const PacketUser:User | undefined = users.getByToken(requestTokenString); const PacketUser:User | undefined = shared.users.getByToken(requestTokenString);
// Make sure the client's token isn't invalid // Make sure the client's token isn't invalid
if (PacketUser != null) { if (PacketUser != null) {
@ -144,7 +114,7 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
PacketUser.timeoutTime = Date.now() + 60000; PacketUser.timeoutTime = Date.now() + 60000;
// Create a new osu! packet reader // Create a new osu! packet reader
const osuPacketReader = new osu.Client.Reader(packet); const osuPacketReader = osu.Client.Reader(packet);
// Parse current bancho packet // Parse current bancho packet
const PacketData = osuPacketReader.Parse(); const PacketData = osuPacketReader.Parse();
@ -156,7 +126,8 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break; break;
case Packets.Client_SendPublicMessage: case Packets.Client_SendPublicMessage:
let channel = chatManager.GetChannelByName(CurrentPacket.data.target); const message:MessageData = CurrentPacket.data;
let channel = shared.chatManager.GetChannelByName(message.target);
if (channel instanceof Channel) { if (channel instanceof Channel) {
channel.SendMessage(PacketUser, CurrentPacket.data.message); channel.SendMessage(PacketUser, CurrentPacket.data.message);
} }
@ -183,23 +154,23 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break; break;
case Packets.Client_SendPrivateMessage: case Packets.Client_SendPrivateMessage:
//SendPrivateMessage(PacketUser, CurrentPacket.data); PrivateMessage(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_JoinLobby: case Packets.Client_JoinLobby:
multiplayerManager.JoinLobby(PacketUser); shared.multiplayerManager.JoinLobby(PacketUser);
break; break;
case Packets.Client_PartLobby: case Packets.Client_PartLobby:
multiplayerManager.LeaveLobby(PacketUser); shared.multiplayerManager.LeaveLobby(PacketUser);
break; break;
case Packets.Client_CreateMatch: case Packets.Client_CreateMatch:
await multiplayerManager.CreateMatch(PacketUser, CurrentPacket.data); await shared.multiplayerManager.CreateMatch(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_JoinMatch: case Packets.Client_JoinMatch:
multiplayerManager.JoinMatch(PacketUser, CurrentPacket.data); shared.multiplayerManager.JoinMatch(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_MatchChangeSlot: case Packets.Client_MatchChangeSlot:
@ -284,11 +255,11 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break; break;
case Packets.Client_FriendAdd: case Packets.Client_FriendAdd:
//AddFriend(PacketUser, CurrentPacket.data); AddFriend(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_FriendRemove: case Packets.Client_FriendRemove:
//RemoveFriend(PacketUser, CurrentPacket.data); RemoveFriend(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_UserStatsRequest: case Packets.Client_UserStatsRequest:
@ -296,15 +267,15 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break; break;
case Packets.Client_SpecialMatchInfoRequest: case Packets.Client_SpecialMatchInfoRequest:
//TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data); TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_SpecialJoinMatchChannel: case Packets.Client_SpecialJoinMatchChannel:
//TourneyMatchJoinChannel(PacketUser, CurrentPacket.data); TourneyMatchJoinChannel(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_SpecialLeaveMatchChannel: case Packets.Client_SpecialLeaveMatchChannel:
//TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data); TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data);
break; break;
case Packets.Client_Invite: case Packets.Client_Invite:
@ -328,14 +299,18 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
PacketUser.clearQueue(); PacketUser.clearQueue();
} else { } else {
// Only do this if we're absolutely sure that we're connected to the DB // Only do this if we're absolutely sure that we're connected to the DB
if (DB.connected) { if (shared.database.connected) {
// User's token is invlid, force a reconnect // User's token is invlid, force a reconnect
ConsoleHelper.printBancho(`Forced client re-login (Token is invalid)`); ConsoleHelper.printBancho(`Forced client re-login (Token is invalid)`);
responseData = "\u0005\u0000\u0000\u0004\u0000\u0000\u0000<30><30><EFBFBD><EFBFBD>\u0018\u0000\u0000\u0011\u0000\u0000\u0000\u000b\u000fReconnecting..."; responseData = "\u0005\u0000\u0000\u0004\u0000\u0000\u0000<30><30><EFBFBD><EFBFBD>\u0018\u0000\u0000\u0011\u0000\u0000\u0000\u000b\u000fReconnecting...";
} }
} }
} catch (e) { } catch (e) {
console.error(e); if (Constants.DEBUG) {
throw e;
}
ConsoleHelper.printError(`${e}`);
} finally { } finally {
res.writeHead(200, { res.writeHead(200, {
"Connection": "keep-alive", "Connection": "keep-alive",

34
server/Bot.ts Normal file
View file

@ -0,0 +1,34 @@
import { ICommand } from "./interfaces/ICommand";
import { Channel } from "./objects/Channel";
import { Shared } from "./objects/Shared";
import { User } from "./objects/User";
// Commands
import { RankingCommand } from "./commands/Ranking";
import { LockCommand } from "./commands/Lock";
import { MultiplayerCommands } from "./commands/Multiplayer";
import { HelpCommand } from "./commands/Help";
import { RollCommand } from "./commands/RollCommand";
export class Bot {
public user:User;
private commands:{ [id: string]: ICommand } = {};
public constructor(shared:Shared, botUser:User) {
this.user = botUser;
this.commands["help"] = new HelpCommand(shared, this.commands);
this.commands["ranking"] = new RankingCommand(shared);
this.commands["lock"] = new LockCommand(shared);
this.commands["mp"] = new MultiplayerCommands(shared);
this.commands["roll"] = new RollCommand(shared);
}
public OnMessage(channel:Channel, sender:User, text:string) {
const args = text.split(" ");
const command = this.commands[`${args.shift()?.replace("!", "").toLowerCase()}`];
if (command) {
command.exec(channel, sender, args);
}
}
}

View file

@ -2,21 +2,22 @@ import { Channel } from "./objects/Channel";
import { ConsoleHelper } from "../ConsoleHelper"; import { ConsoleHelper } from "../ConsoleHelper";
import { FunkyArray } from "./objects/FunkyArray"; import { FunkyArray } from "./objects/FunkyArray";
import { User } from "./objects/User"; import { User } from "./objects/User";
import { SharedContent } from "./interfaces/SharedContent"; import { Shared } from "./objects/Shared";
const osu = require("osu-packet"); import { osu } from "../osuTyping";
import { PrivateChannel } from "./objects/PrivateChannel";
export class ChatManager { export class ChatManager {
public chatChannels:FunkyArray<Channel> = new FunkyArray<Channel>(); public chatChannels:FunkyArray<Channel> = new FunkyArray<Channel>();
public forceJoinChannels:FunkyArray<Channel> = new FunkyArray<Channel>(); public forceJoinChannels:FunkyArray<Channel> = new FunkyArray<Channel>();
private readonly sharedContent:SharedContent; private readonly shared:Shared;
public constructor(sharedContent:SharedContent) { public constructor(shared:Shared) {
this.sharedContent = sharedContent; this.shared = shared;
} }
public AddChatChannel(name:string, description:string, forceJoin:boolean = false) : Channel { public AddChatChannel(name:string, description:string, forceJoin:boolean = false) : Channel {
const stream = this.sharedContent.streams.CreateStream(`chat_channel:${name}`, false); const stream = this.shared.streams.CreateStream(`chat_channel:${name}`, false);
const channel = new Channel(this.sharedContent, `#${name}`, description, stream); const channel = new Channel(this.shared, `#${name}`, description, stream);
this.chatChannels.add(channel.name, channel); this.chatChannels.add(channel.name, channel);
if (forceJoin) { if (forceJoin) {
this.forceJoinChannels.add(name, channel); this.forceJoinChannels.add(name, channel);
@ -26,8 +27,8 @@ export class ChatManager {
} }
public AddSpecialChatChannel(name:string, streamName:string, forceJoin:boolean = false) : Channel { public AddSpecialChatChannel(name:string, streamName:string, forceJoin:boolean = false) : Channel {
const stream = this.sharedContent.streams.CreateStream(`chat_channel:${streamName}`, false); const stream = this.shared.streams.CreateStream(`chat_channel:${streamName}`, false);
const channel = new Channel(this.sharedContent, `#${name}`, "", stream); const channel = new Channel(this.shared, `#${name}`, "", stream);
this.chatChannels.add(channel.name, channel); this.chatChannels.add(channel.name, channel);
if (forceJoin) { if (forceJoin) {
this.forceJoinChannels.add(name, channel); this.forceJoinChannels.add(name, channel);
@ -51,18 +52,31 @@ export class ChatManager {
} }
} }
public AddPrivateChatChannel(user0:User, user1:User) {
const stream = this.shared.streams.CreateStream(`private_channel:${user0.username},${user1.username}`, true);
const channel = new PrivateChannel(user0, user1, stream);
this.chatChannels.add(channel.name, channel);
ConsoleHelper.printChat(`Created private chat channel [${channel.name}]`);
return channel;
}
public GetChannelByName(channelName:string) : Channel | undefined { public GetChannelByName(channelName:string) : Channel | undefined {
return this.chatChannels.getByKey(channelName); return this.chatChannels.getByKey(channelName);
} }
public GetPrivateChannelByName(channelName:string) : Channel | undefined {
return this.chatChannels.getByKey(channelName);
}
public ForceJoinChannels(user:User) { public ForceJoinChannels(user:User) {
for (let channel of this.forceJoinChannels.getIterableItems()) { for (let channel of this.forceJoinChannels.getIterableItems()) {
channel.Join(user); channel.Join(user);
} }
} }
public SendChannelListing(user:User) { public SendChannelListing(user:User) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
for (let channel of this.chatChannels.getIterableItems()) { for (let channel of this.chatChannels.getIterableItems()) {
if (channel.isSpecial) { if (channel.isSpecial) {
continue; continue;

View file

@ -1,264 +1,270 @@
const countryCodes = { enum CountryCodes {
LV: 132, LV = 132,
AD: 3, AD = 3,
LT: 130, LT = 130,
KM: 116, KM = 116,
QA: 182, QA = 182,
VA: 0, VA = 0,
PK: 173, PK = 173,
KI: 115, KI = 115,
SS: 0, SS = 0,
KH: 114, KH = 114,
NZ: 166, NZ = 166,
TO: 215, TO = 215,
KZ: 122, KZ = 122,
GA: 76, BW = 35,
BW: 35, GA = 76,
AX: 247, AX = 247,
GE: 79, GE = 79,
UA: 222, UA = 222,
CR: 50, CR = 50,
AE: 0, AE = 0,
NE: 157, NE = 157,
ZA: 240, ZA = 240,
SK: 196, SK = 196,
BV: 34, BV = 34,
SH: 0, SH = 0,
PT: 179, PT = 179,
SC: 189, SC = 189,
CO: 49, CO = 49,
GP: 86, GP = 86,
GY: 93, GY = 93,
CM: 47, CM = 47,
TJ: 211, TJ = 211,
AF: 5, AF = 5,
IE: 101, IE = 101,
AL: 8, AL = 8,
BG: 24, BG = 24,
JO: 110, JO = 110,
MU: 149, MU = 149,
PM: 0, PM = 0,
LA: 0, LA = 0,
IO: 104, IO = 104,
KY: 121, KY = 121,
SA: 187, SA = 187,
KN: 0, KN = 0,
OM: 167, OM = 167,
CY: 54, CY = 54,
BQ: 0, BQ = 0,
BT: 33, BT = 33,
WS: 236, WS = 236,
ES: 67, ES = 67,
LR: 128, LR = 128,
RW: 186, RW = 186,
AQ: 12, AQ = 12,
PW: 180, PW = 180,
JE: 250, JE = 250,
TN: 214, TN = 214,
ZW: 243, ZW = 243,
JP: 111, JP = 111,
BB: 20, BB = 20,
VN: 233, VN = 233,
HN: 96, HN = 96,
KP: 0, KP = 0,
WF: 235, WF = 235,
EC: 62, EC = 62,
HU: 99, HU = 99,
GF: 80, GF = 80,
GQ: 87, GQ = 87,
TW: 220, TW = 220,
MC: 135, MC = 135,
BE: 22, BE = 22,
PN: 176, PN = 176,
SZ: 205, SZ = 205,
CZ: 55, CZ = 55,
LY: 0, LY = 0,
IN: 103, IN = 103,
FM: 0, FM = 0,
PY: 181, PY = 181,
PH: 172, PH = 172,
MN: 142, MN = 142,
GG: 248, GG = 248,
CC: 39, CC = 39,
ME: 242, ME = 242,
DO: 60, DO = 60,
KR: 0, KR = 0,
PL: 174, PL = 174,
MT: 148, MT = 148,
MM: 141, MM = 141,
AW: 17, AW = 17,
MV: 150, MV = 150,
BD: 21, BD = 21,
NR: 164, NR = 164,
AT: 15, AT = 15,
GW: 92, GW = 92,
FR: 74, FR = 74,
LI: 126, LI = 126,
CF: 41, CF = 41,
DZ: 61, DZ = 61,
MA: 134, MA = 134,
VG: 0, VG = 0,
NC: 156, NC = 156,
IQ: 105, IQ = 105,
BN: 0, BN = 0,
BF: 23, BF = 23,
BO: 30, BO = 30,
GB: 77, GB = 77,
CU: 51, CU = 51,
LU: 131, LU = 131,
YT: 238, YT = 238,
NO: 162, NO = 162,
SM: 198, SM = 198,
GL: 83, GL = 83,
IS: 107, IS = 107,
AO: 11, AO = 11,
MH: 138, MH = 138,
SE: 191, SE = 191,
ZM: 241, ZM = 241,
FJ: 70, FJ = 70,
SL: 197, SL = 197,
CH: 43, CH = 43,
RU: 0, RU = 0,
CW: 0, CW = 0,
CX: 53, CX = 53,
TF: 208, TF = 208,
NL: 161, NL = 161,
AU: 16, AU = 16,
FI: 69, FI = 69,
MS: 147, MS = 147,
GH: 81, GH = 81,
BY: 36, BY = 36,
IL: 102, IL = 102,
VC: 0, VC = 0,
NG: 159, NG = 159,
HT: 98, HT = 98,
LS: 129, LS = 129,
MR: 146, MR = 146,
YE: 237, YE = 237,
MP: 144, MP = 144,
SX: 0, SX = 0,
RE: 183, RE = 183,
RO: 184, RO = 184,
NP: 163, NP = 163,
CG: 0, CG = 0,
FO: 73, FO = 73,
CI: 0, CI = 0,
TH: 210, TH = 210,
HK: 94, HK = 94,
TK: 212, TK = 212,
XK: 0, XK = 0,
DM: 59, DM = 59,
LC: 0, LC = 0,
ID: 100, ID = 100,
MG: 137, MG = 137,
JM: 109, JM = 109,
IT: 108, IT = 108,
CA: 38, CA = 38,
TZ: 221, TZ = 221,
GI: 82, GI = 82,
KG: 113, KG = 113,
NU: 165, NU = 165,
TV: 219, TV = 219,
LB: 124, LB = 124,
SY: 0, SY = 0,
PR: 177, PR = 177,
NI: 160, NI = 160,
KE: 112, KE = 112,
MO: 0, MO = 0,
SR: 201, SR = 201,
VI: 0, VI = 0,
SV: 203, SV = 203,
HM: 0, HM = 0,
CD: 0, CD = 0,
BI: 26, BI = 26,
BM: 28, BM = 28,
MW: 151, MW = 151,
TM: 213, TM = 213,
GT: 90, GT = 90,
AG: 0, AG = 0,
UM: 0, UM = 0,
US: 225, US = 225,
AR: 13, AR = 13,
DJ: 57, DJ = 57,
KW: 120, KW = 120,
MY: 153, MY = 153,
FK: 71, FK = 71,
EG: 64, EG = 64,
BA: 0, BA = 0,
CN: 48, CN = 48,
GN: 85, GN = 85,
PS: 178, PS = 178,
SO: 200, SO = 200,
IM: 249, IM = 249,
GS: 0, GS = 0,
BR: 31, BR = 31,
GM: 84, GM = 84,
PF: 170, PF = 170,
PA: 168, PA = 168,
PG: 171, PG = 171,
BH: 25, BH = 25,
TG: 209, TG = 209,
GU: 91, GU = 91,
CK: 45, CK = 45,
MF: 252, MF = 252,
VE: 230, VE = 230,
CL: 46, CL = 46,
TR: 217, TR = 217,
UG: 223, UG = 223,
GD: 78, GD = 78,
TT: 218, TT = 218,
TL: 0, TL = 0,
MD: 0, MD = 0,
MK: 0, MK = 0,
ST: 202, ST = 202,
CV: 52, CV = 52,
MQ: 145, MQ = 145,
GR: 88, GR = 88,
HR: 97, HR = 97,
BZ: 37, BZ = 37,
UZ: 227, UZ = 227,
DK: 58, DK = 58,
SN: 199, SN = 199,
ET: 68, ET = 68,
VU: 234, VU = 234,
ER: 66, ER = 66,
BJ: 27, BJ = 27,
LK: 127, LK = 127,
NA: 155, NA = 155,
AS: 14, AS = 14,
SG: 192, SG = 192,
PE: 169, PE = 169,
IR: 0, IR = 0,
MX: 152, MX = 152,
TD: 207, TD = 207,
AZ: 18, AZ = 18,
AM: 9, AM = 9,
BL: 0, BL = 0,
SJ: 195, SJ = 195,
SB: 188, SB = 188,
NF: 158, NF = 158,
RS: 239, RS = 239,
DE: 56, DE = 56,
EH: 65, EH = 65,
EE: 63, EE = 63,
SD: 190, SD = 190,
ML: 140, ML = 140,
TC: 206, TC = 206,
MZ: 154, MZ = 154,
BS: 32, BS = 32,
UY: 226, UY = 226,
SI: 194, SI = 194,
AI: 7 AI = 7
}; };
const countryCodeKeys = Object.keys(countryCodes); const keys = Object.keys(CountryCodes);
const values = Object.values(CountryCodes);
export function getCountryID(code:string) : number { export function getCountryID(code:string) : number {
// Get id of a country from a 2 char code // Get id of a country from a 2 char code
/*const upperCode:string = code.toUpperCase(); const upperCode:string = code.toUpperCase();
if (code in countryCodes) { if (upperCode in CountryCodes) {
return countryCodes[upperCode]; const code = values[keys.indexOf(upperCode)];
}*/ if (typeof(code) === "string") {
return 0;
}
return code;
}
return 0; return 0;
} }

View file

@ -1,5 +1,4 @@
import { ConsoleHelper } from "../ConsoleHelper"; import { ConsoleHelper } from "../ConsoleHelper";
import { Database } from "./objects/Database";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { getCountryID } from "./Country"; import { getCountryID } from "./Country";
import { generateSession } from "./Util"; import { generateSession } from "./Util";
@ -7,46 +6,21 @@ import { LatLng } from "./objects/LatLng";
import { LoginInfo } from "./objects/LoginInfo"; import { LoginInfo } from "./objects/LoginInfo";
import { Logout } from "./packets/Logout"; import { Logout } from "./packets/Logout";
import { pbkdf2 } from "crypto"; import { pbkdf2 } from "crypto";
import { readFileSync } from "fs";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { UserArray } from "./objects/UserArray";
import { User } from "./objects/User"; import { User } from "./objects/User";
import { DataStreamArray } from "./objects/DataStreamArray";
import { ChatManager } from "./ChatManager";
import { UserPresenceBundle } from "./packets/UserPresenceBundle"; import { UserPresenceBundle } from "./packets/UserPresenceBundle";
import { UserPresence } from "./packets/UserPresence"; import { UserPresence } from "./packets/UserPresence";
import { StatusUpdate } from "./packets/StatusUpdate"; import { StatusUpdate } from "./packets/StatusUpdate";
import { SharedContent } from "./interfaces/SharedContent"; import { Shared } from "./objects/Shared";
const config:any = JSON.parse(readFileSync("./config.json").toString()); import { osu } from "../osuTyping";
import { IpZxqResponse } from "./interfaces/IpZxqResponse";
const { decrypt: aesDecrypt } = require("aes256"); const { decrypt: aesDecrypt } = require("aes256");
const osu = require("osu-packet");
function incorrectLoginResponse() { const incorrectLoginResponse:Buffer = osu.Bancho.Writer().LoginReply(-1).toBuffer;
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.LoginReply(-1);
return [
osuPacketWriter.toBuffer,
{
'cho-protocol': 19,
'Connection': 'keep-alive',
'Keep-Alive': 'timeout=5, max=100',
}
];
}
function requiredPWChangeResponse() { const requiredPWChangeResponse:Buffer = osu.Bancho.Writer()
const osuPacketWriter = new osu.Bancho.Writer; .LoginReply(-1)
osuPacketWriter.Announce("As part of migration to a new password system you are required to change your password. Please log in on the website and change your password."); .Announce("As part of migration to a new password system you are required to change your password. Please logon to the website and change your password.").toBuffer;
osuPacketWriter.LoginReply(-1);
return [
osuPacketWriter.toBuffer,
{
'cho-protocol': 19,
'Connection': 'keep-alive',
'Keep-Alive': 'timeout=5, max=100',
}
];
}
enum LoginTypes { enum LoginTypes {
CURRENT, CURRENT,
@ -54,172 +28,210 @@ enum LoginTypes {
OLD_AES OLD_AES
} }
function TestLogin(loginInfo:LoginInfo | undefined, database:Database) { enum LoginResult {
return new Promise(async (resolve, reject) => { VALID,
// Check if there is any login information provided MIGRATION,
if (loginInfo == null) return resolve(incorrectLoginResponse()); INCORRECT,
}
const userDBData:any = await database.query("SELECT * FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]); function TestLogin(loginInfo:LoginInfo, shared:Shared) {
return new Promise<LoginResult>(async (resolve, reject) => {
const userDBData:any = await shared.database.query("SELECT * FROM users_info WHERE username = ? LIMIT 1", [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(incorrectLoginResponse()); if (userDBData == null) return resolve(LoginResult.INCORRECT);
// Make sure the username is the same as the login info // Make sure the username is the same as the login info
if (userDBData.username !== loginInfo.username) return resolve(incorrectLoginResponse()); if (userDBData.username !== loginInfo.username) return resolve(LoginResult.INCORRECT);
/*
1: Old MD5 password console.log(userDBData.has_old_password);
2: Old AES password
*/
switch (userDBData.has_old_password) { switch (userDBData.has_old_password) {
case LoginTypes.CURRENT: case LoginTypes.CURRENT:
pbkdf2(loginInfo.password, userDBData.password_salt, config.database.pbkdf2.itterations, config.database.pbkdf2.keylength, "sha512", (err, derivedKey) => { pbkdf2(loginInfo.password, userDBData.password_salt, shared.config.database.pbkdf2.itterations, shared.config.database.pbkdf2.keylength, "sha512", (err, derivedKey) => {
if (err) { if (err) {
return reject(err); return reject(err);
} else { } else {
if (derivedKey.toString("hex") !== userDBData.password_hash) if (derivedKey.toString("hex") !== userDBData.password_hash)
return resolve(incorrectLoginResponse()); return resolve(LoginResult.INCORRECT);
return resolve(undefined); // We good return resolve(LoginResult.VALID); // We good
} }
}); });
break; break;
case LoginTypes.OLD_AES: case LoginTypes.OLD_AES:
if (aesDecrypt(config.database.key, userDBData.password_hash) !== loginInfo.password) { console.log("OLD AES");
return resolve(resolve(incorrectLoginResponse())); if (aesDecrypt(shared.config.database.key, userDBData.password_hash) !== loginInfo.password) {
return resolve(LoginResult.INCORRECT);
} }
return resolve(requiredPWChangeResponse()); console.log("correct password");
return resolve(LoginResult.MIGRATION);
case LoginTypes.OLD_MD5: case LoginTypes.OLD_MD5:
if (userDBData.password_hash !== loginInfo.password) { if (userDBData.password_hash !== loginInfo.password) {
return resolve(incorrectLoginResponse()); return resolve(LoginResult.INCORRECT);
} }
return resolve(requiredPWChangeResponse()); return resolve(LoginResult.MIGRATION);
} }
}); });
} }
export async function LoginProcess(req:Request, res:Response, packet:Buffer, sharedContent:SharedContent) { export async function LoginProcess(req:Request, res:Response, packet:Buffer, shared:Shared) {
const loginInfo = LoginInfo.From(packet);
const loginStartTime = Date.now(); const loginStartTime = Date.now();
const loginInfo = LoginInfo.From(packet);
const loginCheck:any = await TestLogin(loginInfo, sharedContent.database); // Send back no data if there's no loginInfo
if (loginCheck != null) { // Somebody is doing something funky
res.writeHead(200, loginCheck[1]); if (loginInfo === undefined) {
return res.end(loginCheck[0]); return res.end("");
} }
if (loginInfo == null) const loginResult:LoginResult = await TestLogin(loginInfo, shared);
return; let osuPacketWriter = osu.Bancho.Writer();
let newUser:User | undefined;
let friendsPresence:Buffer = Buffer.alloc(0);
ConsoleHelper.printBancho(`New client connection. [User: ${loginInfo.username}]`); if (loginResult === LoginResult.VALID && loginInfo !== undefined) {
ConsoleHelper.printBancho(`New client connection. [User: ${loginInfo.username}]`);
// Get users IP for getting location // Get users IP for getting location
// Get cloudflare requestee IP first // Get cloudflare requestee IP first
let requestIP = req.get("cf-connecting-ip"); let requestIP = req.get("cf-connecting-ip");
// Get IP of requestee since we are probably behind a reverse proxy // Get IP of requestee since we are probably behind a reverse proxy
if (requestIP == null) if (requestIP === undefined) {
requestIP = req.get("X-Real-IP"); requestIP = req.get("X-Real-IP");
// Just get the requestee IP (we are not behind a reverse proxy)
// if (requestIP == null)
// requestIP = req.remote_addr;
// Make sure requestIP is never null
if (requestIP == null)
requestIP = "";
let userCountryCode:string, userLocation:LatLng;
// Check if it is a local or null IP
if (!requestIP.includes("192.168.") && !requestIP.includes("127.0.") && requestIP != "") {
// Set location to null island
userCountryCode = "XX";
userLocation = new LatLng(0, 0);
} else {
// Get user's location using zxq
const userLocationRequest = await fetch(`https://ip.zxq.co/${requestIP}`);
const userLocationData:any = await userLocationRequest.json();
const userLatLng:Array<string> = userLocationData.loc.split(",");
userCountryCode = userLocationData.country;
userLocation = new LatLng(parseFloat(userLatLng[0]), parseFloat(userLatLng[1]));
}
// Get information about the user from the database
const userDB = await sharedContent.database.query("SELECT id FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
// Create a token for the client
const newClientToken:string = await generateSession();
const isTourneyClient = loginInfo.version.includes("tourney");
// Make sure user is not already connected, kick off if so.
const connectedUser = sharedContent.users.getByUsername(loginInfo.username);
if (connectedUser != null && !isTourneyClient && !connectedUser.isTourneyUser) {
Logout(connectedUser);
}
// Retreive the newly created user
const newUser:User = sharedContent.users.add(newClientToken, new User(userDB.id, loginInfo.username, newClientToken, sharedContent));
// Set tourney client flag
newUser.isTourneyUser = isTourneyClient;
newUser.location = userLocation;
// Get user's data from the database
newUser.updateUserInfo();
try {
// Save the country id for the same reason as above
newUser.countryID = getCountryID(userCountryCode);
// We're ready to start putting together a login packet
// Create an osu! Packet writer
let osuPacketWriter = new osu.Bancho.Writer;
// The reply id is the user's id in any other case than an error in which case negative numbers are used
osuPacketWriter.LoginReply(newUser.id);
// Current bancho protocol version. Defined in Binato.js
osuPacketWriter.ProtocolNegotiation(19);
// Permission level 4 is osu!supporter
osuPacketWriter.LoginPermissions(4);
// After sending the user their friends list send them the online users
UserPresenceBundle(newUser);
// Set title screen image
//osuPacketWriter.TitleUpdate("http://puu.sh/jh7t7/20c04029ad.png|https://osu.ppy.sh/news/123912240253");
// Add user panel data packets
UserPresence(newUser, newUser.id);
StatusUpdate(newUser, newUser.id);
// peppy pls, why
osuPacketWriter.ChannelListingComplete();
// Setup chat
sharedContent.chatManager.ForceJoinChannels(newUser);
sharedContent.chatManager.SendChannelListing(newUser);
// Construct user's friends list
const userFriends = await sharedContent.database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]);
const friendsArray:Array<number> = new Array<number>();
for (let i = 0; i < userFriends.length; i++) {
friendsArray.push(userFriends[i].friendsWith);
} }
// Send user's friends list
osuPacketWriter.FriendsList(friendsArray);
osuPacketWriter.Announce(`Welcome back ${loginInfo.username}!`); // Just get the requestee IP (we are not behind a reverse proxy)
// if (requestIP == null)
// requestIP = req.remote_addr;
res.removeHeader('X-Powered-By'); // Make sure requestIP is never undefined
res.removeHeader('Date'); if (requestIP === undefined) {
// Complete login requestIP = "";
}
let userCountryCode:string, userLocation:LatLng;
// Check if it is a local or null IP
if (requestIP.includes("192.168.") || requestIP.includes("127.0.") || requestIP === "") {
// Set location to null island
userCountryCode = "XX";
userLocation = new LatLng(0, 0);
} else {
// Get user's location using zxq
const userLocationRequest = await fetch(`https://ip.zxq.co/${requestIP}`);
const userLocationData:IpZxqResponse = await userLocationRequest.json();
const userLatLng = userLocationData.loc.split(",");
userCountryCode = userLocationData.country;
userLocation = new LatLng(parseFloat(userLatLng[0]), parseFloat(userLatLng[1]));
}
// Get information about the user from the database
const userDB = await shared.database.query("SELECT id FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
// Create a token for the client
const newClientToken:string = await generateSession();
const isTourneyClient = loginInfo.version.includes("tourney");
// Make sure user is not already connected, kick off if so.
const connectedUser = shared.users.getByUsername(loginInfo.username);
if (connectedUser != null && !isTourneyClient && !connectedUser.isTourneyUser) {
Logout(connectedUser);
}
// Retreive the newly created user
newUser = shared.users.add(newClientToken, new User(userDB.id, loginInfo.username, newClientToken, shared));
// Set tourney client flag
newUser.isTourneyUser = isTourneyClient;
newUser.location = userLocation;
// Get user's data from the database
newUser.updateUserInfo();
try {
newUser.countryID = getCountryID(userCountryCode);
// We're ready to start putting together a login response
// The reply id is the user's id in any other case than an error in which case negative numbers are used
osuPacketWriter.LoginReply(newUser.id);
osuPacketWriter.ProtocolNegotiation(19);
// Permission level 4 is osu!supporter
osuPacketWriter.LoginPermissions(4);
// Set title screen image
//osuPacketWriter.TitleUpdate("http://puu.sh/jh7t7/20c04029ad.png|https://osu.ppy.sh/news/123912240253");
// Add user panel data packets
UserPresence(newUser, newUser.id);
StatusUpdate(newUser, newUser.id);
// peppy pls, why
osuPacketWriter.ChannelListingComplete();
// Setup chat
shared.chatManager.ForceJoinChannels(newUser);
shared.chatManager.SendChannelListing(newUser);
// Construct & send user's friends list
const userFriends = await shared.database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]);
const friendsArray:Array<number> = new Array<number>();
for (let useFriend of userFriends) {
const friendId:number = useFriend.friendsWith;
friendsArray.push(friendId);
// Also fetch presence for friend if they are online
if (shared.users.getById(friendId) === undefined) { continue; }
const friendPresence = UserPresence(shared, friendId);
if (friendPresence === undefined) { continue; }
friendsPresence = Buffer.concat([
friendsPresence,
friendPresence
], friendsPresence.length + friendPresence.length);
}
osuPacketWriter.FriendsList(friendsArray);
// After sending the user their friends list send them the online users
UserPresenceBundle(newUser);
osuPacketWriter.Announce(`Welcome back ${loginInfo.username}!`);
// TODO: Remove once merged into master
osuPacketWriter.Announce("WARNING\nThis is a development test server made for the TypeScript rewrite.\nAnything could happen be it data loss, catastrophic crashes or otherwise.\nHere be dragons.");
} catch (err) {
console.error(err);
}
}
res.removeHeader('X-Powered-By');
res.removeHeader('Date');
// Complete / Fail login
const writerBuffer:Buffer = osuPacketWriter.toBuffer;
if (newUser === undefined) {
res.writeHead(200, {
"Connection": "keep-alive",
"Keep-Alive": "timeout=5, max=100"
});
console.log(res.headersSent);
switch (loginResult) {
case LoginResult.INCORRECT:
res.end(incorrectLoginResponse, () => {
ConsoleHelper.printBancho(`User login failed (Incorrect Password) took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
});
break;
case LoginResult.MIGRATION:
res.end(requiredPWChangeResponse, () => {
ConsoleHelper.printBancho(`User login failed (Migration Required) took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
});
break;
}
} else {
res.writeHead(200, { res.writeHead(200, {
"cho-token": newUser.uuid, "cho-token": newUser.uuid,
"Connection": "keep-alive", "Connection": "keep-alive",
"Keep-Alive": "timeout=5, max=100", "Keep-Alive": "timeout=5, max=100",
}); });
res.end(osuPacketWriter.toBuffer, () => { res.end(writerBuffer, () => {
ConsoleHelper.printBancho(`User login finished, took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`); ConsoleHelper.printBancho(`User login finished, took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
}); });
} catch (err) {
console.error(err);
} }
} }

View file

@ -1,5 +1,5 @@
import { Channel } from "./objects/Channel"; import { Channel } from "./objects/Channel";
import { SharedContent } from "./interfaces/SharedContent"; import { Shared } from "./objects/Shared";
import { SlotStatus } from "./enums/SlotStatus"; import { SlotStatus } from "./enums/SlotStatus";
import { DataStream } from "./objects/DataStream"; import { DataStream } from "./objects/DataStream";
import { Match } from "./objects/Match"; import { Match } from "./objects/Match";
@ -10,18 +10,18 @@ import { UserPresenceBundle } from "./packets/UserPresenceBundle";
import { MatchArray } from "./objects/MatchArray"; import { MatchArray } from "./objects/MatchArray";
import { MatchJoinData } from "./interfaces/MatchJoinData"; import { MatchJoinData } from "./interfaces/MatchJoinData";
import { MatchData } from "./interfaces/MatchData"; import { MatchData } from "./interfaces/MatchData";
const osu = require("osu-packet"); import { osu } from "../osuTyping";
export class MultiplayerManager { export class MultiplayerManager {
private readonly sharedContent:SharedContent; private readonly shared:Shared;
private matches:MatchArray = new MatchArray(); private matches:MatchArray = new MatchArray();
private readonly lobbyStream:DataStream; private readonly lobbyStream:DataStream;
private readonly lobbyChat:Channel; private readonly lobbyChat:Channel;
public constructor(sharedContent:SharedContent) { public constructor(shared:Shared) {
this.sharedContent = sharedContent; this.shared = shared;
this.lobbyStream = sharedContent.streams.CreateStream("multiplayer:lobby", false); this.lobbyStream = shared.streams.CreateStream("multiplayer:lobby", false);
const channel = this.sharedContent.chatManager.GetChannelByName("#lobby"); const channel = this.shared.chatManager.GetChannelByName("#lobby");
if (channel === undefined) { if (channel === undefined) {
throw "Something has gone horribly wrong, the lobby channel does not exist!"; throw "Something has gone horribly wrong, the lobby channel does not exist!";
} }
@ -84,7 +84,7 @@ export class MultiplayerManager {
match.matchStream.AddUser(user); match.matchStream.AddUser(user);
match.matchChatChannel.Join(user); match.matchChatChannel.Join(user);
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchJoinSuccess(match.generateMatchJSON()); osuPacketWriter.MatchJoinSuccess(match.generateMatchJSON());
@ -92,7 +92,7 @@ export class MultiplayerManager {
this.UpdateLobbyListing(); this.UpdateLobbyListing();
} catch (e) { } catch (e) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchJoinFail(); osuPacketWriter.MatchJoinFail();
@ -107,8 +107,8 @@ export class MultiplayerManager {
} }
public GenerateLobbyListing(user?:User) : Buffer { public GenerateLobbyListing(user?:User) : Buffer {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
let bufferToSend = UserPresenceBundle(this.sharedContent); let bufferToSend = UserPresenceBundle(this.shared);
for (let match of this.matches.getIterableItems()) { for (let match of this.matches.getIterableItems()) {
for (let slot of match.slots) { for (let slot of match.slots) {
@ -116,8 +116,13 @@ export class MultiplayerManager {
continue; continue;
} }
const presenceBuffer = UserPresence(this.sharedContent, slot.player.id); const presenceBuffer = UserPresence(this.shared, slot.player.id);
const statusBuffer = StatusUpdate(this.sharedContent, slot.player.id); const statusBuffer = StatusUpdate(this.shared, slot.player.id);
if (presenceBuffer === undefined || statusBuffer === undefined) {
continue;
}
bufferToSend = Buffer.concat([bufferToSend, presenceBuffer, statusBuffer], bufferToSend.length + presenceBuffer.length + statusBuffer.length); bufferToSend = Buffer.concat([bufferToSend, presenceBuffer, statusBuffer], bufferToSend.length + presenceBuffer.length + statusBuffer.length);
} }
@ -134,8 +139,12 @@ export class MultiplayerManager {
return bufferToSend; return bufferToSend;
} }
public GetMatchById(id:number) : Match | undefined {
return this.matches.getById(id);
}
public async CreateMatch(user:User, matchData:MatchData) { public async CreateMatch(user:User, matchData:MatchData) {
const match = await Match.createMatch(user, matchData, this.sharedContent); const match = await Match.createMatch(user, matchData, this.shared);
this.matches.add(match.matchId.toString(), match); this.matches.add(match.matchId.toString(), match);
this.JoinMatch(user, match.matchId); this.JoinMatch(user, match.matchId);
} }

View file

@ -0,0 +1,41 @@
import { Channel } from "./objects/Channel";
import { ConsoleHelper } from "../ConsoleHelper";
import { FunkyArray } from "./objects/FunkyArray";
import { User } from "./objects/User";
import { Shared } from "./objects/Shared";
import { osu } from "../osuTyping";
import { PrivateChannel } from "./objects/PrivateChannel";
export class PrivateChatManager {
public chatChannels:FunkyArray<PrivateChannel> = new FunkyArray<PrivateChannel>();
private readonly shared:Shared;
public constructor(shared:Shared) {
this.shared = shared;
}
public AddChannel(user0:User, user1:User) : PrivateChannel {
const stream = this.shared.streams.CreateStream(`private_channel:${user0.username},${user1.username}`, true);
const channel = new PrivateChannel(user0, user1, stream);
this.chatChannels.add(channel.name, channel);
ConsoleHelper.printChat(`Created private chat channel [${channel.name}]`);
return channel;
}
public RemoveChannel(channel:PrivateChannel | string) {
if (channel instanceof Channel) {
channel.stream.Delete();
this.chatChannels.remove(channel.stream.name);
} else {
const chatChannel = this.GetChannelByName(channel);
if (chatChannel instanceof Channel) {
chatChannel.stream.Delete();
this.chatChannels.remove(chatChannel.stream.name);
}
}
}
public GetChannelByName(channelName:string) : PrivateChannel | undefined {
return this.chatChannels.getByKey(channelName);
}
}

View file

@ -1,17 +1,17 @@
import { DataStream } from "./objects/DataStream"; import { DataStream } from "./objects/DataStream";
import { SharedContent } from "./interfaces/SharedContent"; import { Shared } from "./objects/Shared";
import { User } from "./objects/User"; import { User } from "./objects/User";
const osu = require("osu-packet"); import { osu } from "../osuTyping";
export class SpectatorManager { export class SpectatorManager {
private sharedContent:SharedContent; private shared:Shared;
public constructor(sharedContent:SharedContent) { public constructor(shared:Shared) {
this.sharedContent = sharedContent; this.shared = shared;
} }
public startSpectating(user:User, userIdToSpectate:number) { public startSpectating(user:User, userIdToSpectate:number) {
const userToSpectate = this.sharedContent.users.getById(userIdToSpectate); const userToSpectate = this.shared.users.getById(userIdToSpectate);
if (userToSpectate === undefined) { if (userToSpectate === undefined) {
return; return;
} }
@ -19,20 +19,20 @@ export class SpectatorManager {
// Use existing or create spectator stream // Use existing or create spectator stream
let spectateStream:DataStream; let spectateStream:DataStream;
if (userToSpectate.spectatorStream === undefined) { if (userToSpectate.spectatorStream === undefined) {
user.spectatorStream = spectateStream = userToSpectate.spectatorStream = this.sharedContent.streams.CreateStream(`spectator:${userToSpectate.username}`); user.spectatorStream = spectateStream = userToSpectate.spectatorStream = this.shared.streams.CreateStream(`spectator:${userToSpectate.username}`);
} else { } else {
user.spectatorStream = spectateStream = userToSpectate.spectatorStream; user.spectatorStream = spectateStream = userToSpectate.spectatorStream;
} }
user.spectatingUser = userToSpectate; user.spectatingUser = userToSpectate;
let osuPacketWriter = new osu.Bancho.Writer; let osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectatorJoined(user.id); osuPacketWriter.SpectatorJoined(user.id);
userToSpectate.addActionToQueue(osuPacketWriter.toBuffer); userToSpectate.addActionToQueue(osuPacketWriter.toBuffer);
osuPacketWriter = new osu.Bancho.Writer; osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.FellowSpectatorJoined(user.id); osuPacketWriter.FellowSpectatorJoined(user.id);
@ -45,7 +45,7 @@ export class SpectatorManager {
return; return;
} }
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectateFrames(spectateFrameData); osuPacketWriter.SpectateFrames(spectateFrameData);
user.spectatorStream.Send(osuPacketWriter.toBuffer); user.spectatorStream.Send(osuPacketWriter.toBuffer);
@ -58,7 +58,7 @@ export class SpectatorManager {
const spectatedUser = user.spectatingUser; const spectatedUser = user.spectatingUser;
let osuPacketWriter = new osu.Bancho.Writer; let osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectatorLeft(user.id); osuPacketWriter.SpectatorLeft(user.id);
spectatedUser.addActionToQueue(osuPacketWriter.toBuffer); spectatedUser.addActionToQueue(osuPacketWriter.toBuffer);
@ -69,7 +69,7 @@ export class SpectatorManager {
user.spectatingUser = undefined; user.spectatingUser = undefined;
if (stream.IsActive) { if (stream.IsActive) {
osuPacketWriter = new osu.Bancho.Writer; osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.FellowSpectatorLeft(user.id); osuPacketWriter.FellowSpectatorLeft(user.id);
stream.Send(osuPacketWriter.toBuffer); stream.Send(osuPacketWriter.toBuffer);
} else { } else {

View file

@ -1,12 +1,3 @@
// 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);
}
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
export function generateSession() : Promise<string> { export function generateSession() : Promise<string> {

View file

@ -0,0 +1,19 @@
import { ICommand } from "../interfaces/ICommand";
import { Channel } from "../objects/Channel";
import { Shared } from "../objects/Shared";
import { User } from "../objects/User";
export class BaseCommand implements ICommand {
public shared:Shared;
public readonly helpText:string = "No help page was found for that command";
public readonly helpDescription:string = "Command has no description set";
public readonly helpArguments:Array<string> = new Array<string>();
public constructor(shared:Shared) {
this.shared = shared;
}
public exec(channel:Channel, sender:User, args:Array<string>) {
}
}

41
server/commands/Help.ts Normal file
View file

@ -0,0 +1,41 @@
import { Channel } from "../objects/Channel";
import { User } from "../objects/User";
import { BaseCommand } from "./BaseCommand";
import { Shared } from "../objects/Shared";
import { ICommand } from "../interfaces/ICommand";
export class HelpCommand extends BaseCommand {
public readonly helpDescription:string = "Shows this message! :)";
private readonly commandList:{ [id:string]: ICommand };
private commandKeys:Array<string> = new Array<string>();
public constructor(shared:Shared, commands:{ [id:string]: ICommand }) {
super(shared);
this.commandList = commands;
}
public exec(channel:Channel, sender:User, args:Array<string>) {
if (this.commandKeys.length === 0) {
this.commandKeys = Object.keys(this.commandList);
}
// All commands
if (args.length === 0) {
let constructedHelp = "Help:\n";
for (let key of this.commandKeys) {
constructedHelp += ` !${key} - ${this.commandList[key].helpDescription}\n`;
}
channel.SendBotMessage(constructedHelp.slice(0, constructedHelp.length - 1));
return;
}
// Command page
const commandName = args[0].toLowerCase();
if (commandName in this.commandList) {
channel.SendBotMessage(this.commandList[commandName].helpText);
} else {
channel.SendBotMessage("No help page was found for that command");
}
}
}

17
server/commands/Lock.ts Normal file
View file

@ -0,0 +1,17 @@
import { Channel } from "../objects/Channel";
import { User } from "../objects/User";
import { BaseCommand } from "./BaseCommand";
export class LockCommand extends BaseCommand {
public readonly helpDescription:string = "Locks/Unlocks a channel and limits conversation to mods and above.";
public exec(channel:Channel, sender:User, args:Array<string>) {
if (channel.isSpecial) {
channel.SendBotMessage("Multiplayer channels cannot be locked");
return;
}
channel.isLocked = !channel.isLocked;
channel.SendBotMessage(`Channel is now ${channel.isLocked ? "locked" : "unlocked"}`);
}
}

View file

@ -0,0 +1,98 @@
import { Channel } from "../objects/Channel";
import { User } from "../objects/User";
import { Match } from "../objects/Match";
import { BaseCommand } from "./BaseCommand";
const helpText = `Multiplayer Subcommands:
!mp start - Starts a multiplayer match with a delay (optional)
!mp abort - Aborts the currently running round / countdown
!mp obr - Toggles Battle Royale mode`;
export class MultiplayerCommands extends BaseCommand {
public readonly helpText:string = helpText;
public readonly helpDescription:string = "Command for use in multiplayer matches.";
public readonly helpArguments:Array<string> = ["subCommand"];
public exec(channel:Channel, sender:User, args:Array<string>) {
// TODO: Determine if this check is correct
if (sender.match == undefined || channel.name != "#multiplayer") {
channel.SendBotMessage("You must be in a multiplayer match to use this command");
return;
}
// Check if sender is match host
if (!User.Equals(sender, sender.match.host)) {
channel.SendBotMessage("You must be the match host to use multiplayer commands");
return;
}
if (args.length === 0) {
channel.SendBotMessage("You must specify a sub command, use \"!help mp\" to see a list of them.");
return;
}
const subCommand = args[0].toLowerCase();
args.shift();
switch (subCommand) {
case "start":
return mpStart(channel, sender.match, args);
case "abort":
return mpAbort(channel, sender.match);
case "obr":
return mpOBR(channel, sender.match);
}
}
}
function mpStart(channel:Channel, match:Match, args:Array<string>) {
// If no time is specified start instantly
if (args.length === 0) {
channel.SendBotMessage("Good luck, have fun!");
setTimeout(() => match.startMatch(), 1000);
return;
}
const countdownTime = parseInt(args[0]);
if (isNaN(countdownTime)) {
channel.SendBotMessage("Countdown time must be a valid number");
return;
}
let countdownUpdates = 0;
match.countdownTime = countdownTime;
match.countdownTimer = setInterval(() => {
if (match.countdownTime <= 0) {
clearInterval(match.countdownTimer);
match.countdownTimer = undefined;
channel.SendBotMessage("Good luck, have fun!");
setTimeout(() => match.startMatch(), 1000);
return;
}
if (match.countdownTime <= 5 && match.countdownTime > 0) {
channel.SendBotMessage(`Starting in ${match.countdownTime} seconds`);
} else if (match.countdownTime <= 30 ? countdownUpdates % 10 === 0 : countdownUpdates % 30 === 0) {
channel.SendBotMessage(`Starting in ${match.countdownTime} seconds`);
}
match.countdownTime--;
countdownUpdates++;
}, 1000);
}
function mpAbort(channel:Channel, match:Match) {
if (match.countdownTimer && match.countdownTime > 0) {
clearInterval(match.countdownTimer);
match.countdownTimer = undefined;
channel.SendBotMessage("Aborted countdown");
} else {
// TODO: Determine the correct way to abort a round
match.finishMatch();
channel.SendBotMessage("Aborted current round");
}
}
function mpOBR(channel:Channel, match:Match) {
}

View file

@ -0,0 +1,37 @@
import { Channel } from "../objects/Channel";
import { User } from "../objects/User";
import { RankingModes } from "../enums/RankingModes";
import { BaseCommand } from "./BaseCommand";
const helpText = `Ranking Modes:
!ranking pp - Sets your ranking mode to pp
!ranking score - Sets your ranking mode to score
!ranking acc - Sets your ranking mode to accuracy`;
export class RankingCommand extends BaseCommand {
public readonly helpText:string = helpText;
public readonly helpDescription:string = "Sets your prefered ranking type";
public exec(channel:Channel, sender:User, args:Array<string>) {
if (args.length === 0) {
channel.SendBotMessage("You must specify a ranking mode, use \"!help ranking\" to see the options.");
return;
}
switch (args[0].toLowerCase()) {
case "pp":
sender.rankingMode = RankingModes.PP;
channel.SendBotMessage("Set ranking mode to pp.");
break;
case "score":
sender.rankingMode = RankingModes.RANKED_SCORE;
channel.SendBotMessage("Set ranking mode to score.");
break;
case "acc":
sender.rankingMode = RankingModes.AVG_ACCURACY;
channel.SendBotMessage("Set ranking mode to accuracy.");
break;
}
sender.updateUserInfo(true);
}
}

View file

@ -0,0 +1,21 @@
import { Channel } from "../objects/Channel";
import { User } from "../objects/User";
import { BaseCommand } from "./BaseCommand";
export class RollCommand extends BaseCommand {
public readonly helpDescription:string = "Roll some dice and get a random number between 1 and a number (default 100)";
public readonly helpArguments:Array<string> = ["number"];
public exec(channel:Channel, sender:User, args:Array<string>) {
let limit = 99;
if (args.length === 1) {
const userLimit = parseInt(args[0]);
if (!isNaN(userLimit)) {
limit = userLimit;
}
}
const number = Math.round(Math.random() * limit) + 1;
channel.SendBotMessage(`${sender.username} rolls ${number} point(s)`);
}
}

View file

@ -0,0 +1,10 @@
import { Channel } from "../objects/Channel";
import { Shared } from "../objects/Shared";
import { User } from "../objects/User";
export interface ICommand {
shared:Shared,
helpText:string,
helpDescription:string,
exec: (channel:Channel, sender:User, args:Array<string>) => void
}

View file

@ -0,0 +1,4 @@
export interface IpZxqResponse {
country: string,
loc: string
}

View file

@ -0,0 +1,6 @@
export interface MessageData {
sendingClient: string,
message: string,
target: string,
senderId: number
}

View file

@ -0,0 +1,65 @@
import { MatchData } from "./MatchData"
import { MessageData } from "./MessageData"
export interface OsuPacketWriter {
// Functions
LoginReply(data:number) : OsuPacketWriter,
CommandError() : OsuPacketWriter,
SendMessage(data:MessageData) : OsuPacketWriter,
Ping() : OsuPacketWriter,
HandleIrcChangeUsername(data:any) : OsuPacketWriter,
HandleIrcQuit() : OsuPacketWriter,
HandleOsuUpdate(data:any) : OsuPacketWriter,
HandleUserQuit(data:any) : OsuPacketWriter,
SpectatorJoined(data:any) : OsuPacketWriter,
SpectatorLeft(data:any) : OsuPacketWriter,
SpectateFrames(data:any) : OsuPacketWriter,
VersionUpdate() : OsuPacketWriter,
SpectatorCantSpectate(data:any) : OsuPacketWriter,
GetAttention() : OsuPacketWriter,
Announce(data:string) : OsuPacketWriter,
MatchUpdate(data:MatchData) : OsuPacketWriter,
MatchNew(data:any) : OsuPacketWriter,
MatchDisband(data:any) : OsuPacketWriter,
MatchJoinSuccess(data:any) : OsuPacketWriter,
MatchJoinFail() : OsuPacketWriter,
FellowSpectatorJoined(data:any) : OsuPacketWriter,
FellowSpectatorLeft(data:any) : OsuPacketWriter,
MatchStart(data:any) : OsuPacketWriter,
MatchScoreUpdate(data:any) : OsuPacketWriter,
MatchTransferHost(data:any) : OsuPacketWriter,
MatchAllPlayersLoaded() : OsuPacketWriter,
MatchPlayerFailed(data:any) : OsuPacketWriter,
MatchComplete() : OsuPacketWriter,
MatchSkip() : OsuPacketWriter,
Unauthorised() : OsuPacketWriter,
ChannelJoinSuccess(data:any) : OsuPacketWriter,
ChannelAvailable(data:any) : OsuPacketWriter,
ChannelRevoked(data:any) : OsuPacketWriter,
ChannelAvailableAutojoin(data:any) : OsuPacketWriter,
BeatmapInfoReply() : OsuPacketWriter,
LoginPermissions(data:number) : OsuPacketWriter,
FriendsList(data:Array<number>) : OsuPacketWriter,
ProtocolNegotiation(data:number) : OsuPacketWriter,
TitleUpdate(data:string) : OsuPacketWriter,
Monitor() : OsuPacketWriter,
MatchPlayerSkipped(data:any) : OsuPacketWriter,
UserPresence(data:any) : OsuPacketWriter,
Restart(data:any) : OsuPacketWriter,
Invite(data:any) : OsuPacketWriter,
ChannelListingComplete() : OsuPacketWriter,
MatchChangePassword(data:any) : OsuPacketWriter,
BanInfo(data:any) : OsuPacketWriter,
UserSilenced(data:any) : OsuPacketWriter,
UserPresenceSingle(data:any) : OsuPacketWriter,
UserPresenceBundle(data:any) : OsuPacketWriter,
UserPMBlocked(data:any) : OsuPacketWriter,
TargetIsSilenced(data:any) : OsuPacketWriter,
VersionUpdateForced() : OsuPacketWriter,
SwitchServer(data:any) : OsuPacketWriter,
AccountRestricted() : OsuPacketWriter,
RTX(data:any) : OsuPacketWriter,
SwitchTourneyServer(data:any) : OsuPacketWriter
toBuffer : Buffer
}

View file

@ -1,13 +0,0 @@
import { ChatManager } from "../ChatManager";
import { MultiplayerManager } from "../MultiplayerManager";
import { Database } from "../objects/Database";
import { DataStreamArray } from "../objects/DataStreamArray";
import { UserArray } from "../objects/UserArray";
export interface SharedContent {
chatManager:ChatManager,
database:Database,
mutiplayerManager:MultiplayerManager,
streams:DataStreamArray,
users:UserArray,
}

View file

@ -1,77 +1,61 @@
import { SharedContent } from "../interfaces/SharedContent"; import { osu } from "../../osuTyping";
import { Bot } from "../Bot";
import { Shared } from "../objects/Shared";
import { DataStream } from "./DataStream"; import { DataStream } from "./DataStream";
import { User } from "./User"; import { User } from "./User";
const osu = require("osu-packet");
export class Channel { export class Channel {
public name:string; public name:string;
public description:string; public description:string;
public stream:DataStream; public stream:DataStream;
private isLocked:boolean = false; public isLocked:boolean = false;
private _isSpecial:boolean = false; private _isSpecial:boolean = false;
private readonly botUser:User; private readonly bot:Bot;
public constructor(sharedContent:SharedContent, name:string, description:string, stream:DataStream, isSpecial:boolean = false) { public constructor(shared:Shared, name:string, description:string, stream:DataStream, isSpecial:boolean = false) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.stream = stream; this.stream = stream;
this._isSpecial = isSpecial; this._isSpecial = isSpecial;
const bot = sharedContent.users.getByKey("bot"); this.bot = shared.bot;
if (!(bot instanceof User)) {
throw "Something has gone horribly wrong, the bot user doesn't exist!";
}
this.botUser = bot;
} }
public get self() { public get isSpecial() : boolean {
return this;
}
public get isSpecial() {
return this._isSpecial; return this._isSpecial;
} }
public get userCount() { public get userCount() : number {
return this.stream.userCount; return this.stream.userCount;
} }
public SendMessage(sender:User, message:string) { public SendMessage(sender:User, message:string) {
const isBotCommand = message[0] === "!"; if (!this.isLocked) {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({
sendingClient: sender.username,
message: message,
target: this.name,
senderId: sender.id
});
this.stream.SendWithExclusion(osuPacketWriter.toBuffer, sender);
}
if (this.isLocked && !isBotCommand) { if (message[0] === "!") {
this.bot.OnMessage(this, sender, message);
} else if (this.isLocked) {
return this.SendSystemMessage("This channel is currently locked", sender); return this.SendSystemMessage("This channel is currently locked", sender);
} }
if (isBotCommand) {
if (message.split(" ")[0] === "!lock") {
this.isLocked = true;
}
if (message === "!mp start") {
this.SendBotMessage("glhf!");
sender.match?.startMatch();
}
}
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.SendMessage({
sendingClient: sender.username,
message: message,
target: this.name,
senderId: sender.id
});
this.stream.SendWithExclusion(osuPacketWriter.toBuffer, sender);
} }
public SendBotMessage(message:string, sendTo?:User) { public SendBotMessage(message:string, sendTo?:User) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({ osuPacketWriter.SendMessage({
sendingClient: this.botUser.username, sendingClient: this.bot.user.username,
message: message, message: message,
target: this.name, target: this.name,
senderId: this.botUser.id senderId: this.bot.user.id
}); });
if (sendTo instanceof User) { if (sendTo instanceof User) {
@ -82,7 +66,7 @@ export class Channel {
} }
public SendSystemMessage(message:string, sendTo?:User) { public SendSystemMessage(message:string, sendTo?:User) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({ osuPacketWriter.SendMessage({
sendingClient: "System", sendingClient: "System",
message: message, message: message,
@ -99,12 +83,15 @@ export class Channel {
public Join(user:User) { public Join(user:User) {
this.stream.AddUser(user); this.stream.AddUser(user);
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.ChannelJoinSuccess(this.name); osuPacketWriter.ChannelJoinSuccess(this.name);
user.addActionToQueue(osuPacketWriter.toBuffer); user.addActionToQueue(osuPacketWriter.toBuffer);
} }
public Leave(user:User) { public Leave(user:User) {
this.stream.RemoveUser(user); this.stream.RemoveUser(user);
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.ChannelRevoked(this.name);
user.addActionToQueue(osuPacketWriter.toBuffer);
} }
} }

View file

@ -5,12 +5,15 @@ import { User } from "./User";
import { UserArray } from "./UserArray"; import { UserArray } from "./UserArray";
import { hexlify } from "../Util"; import { hexlify } from "../Util";
type DeleteFunction = (dataStream:DataStream) => void;
export class DataStream { export class DataStream {
private users:UserArray = new UserArray(); private users:UserArray = new UserArray();
public readonly name:string; public readonly name:string;
private readonly parent:DataStreamArray; private readonly parent:DataStreamArray;
private readonly removeWhenEmpty:boolean; private readonly removeWhenEmpty:boolean;
private inactive:boolean = false; private inactive:boolean = false;
public onDelete?:DeleteFunction;
public constructor(name:string, parent:DataStreamArray, removeWhenEmpty:boolean) { public constructor(name:string, parent:DataStreamArray, removeWhenEmpty:boolean) {
this.name = name; this.name = name;
@ -32,6 +35,10 @@ export class DataStream {
return this.users.getLength(); return this.users.getLength();
} }
public HasUser(user:User) : boolean {
return this.users.getByKey(user.uuid) !== undefined;
}
public AddUser(user:User) : void { public AddUser(user:User) : void {
this.checkInactive(); this.checkInactive();
@ -54,6 +61,9 @@ export class DataStream {
} }
public Delete() { public Delete() {
if (typeof(this.onDelete) === "function") {
this.onDelete(this);
}
this.parent.DeleteStream(this); this.parent.DeleteStream(this);
} }

View file

@ -7,7 +7,7 @@ export class Database {
public connected:boolean = false; public connected:boolean = false;
public constructor(databaseAddress:string, databasePort:number = 3306, databaseUsername:string, databasePassword:string, databaseName:string, connectedCallback:Function) { public constructor(databaseAddress:string, databasePort:number = 3306, databaseUsername:string, databasePassword:string, databaseName:string) {
this.connectionPool = createPool({ this.connectionPool = createPool({
connectionLimit: Database.CONNECTION_LIMIT, connectionLimit: Database.CONNECTION_LIMIT,
host: databaseAddress, host: databaseAddress,
@ -17,27 +17,7 @@ export class Database {
database: databaseName database: databaseName
}); });
const classCreationTime:number = Date.now(); ConsoleHelper.printInfo(`Connected DB connection pool. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`);
let lastQueryFinished = true;
const connectionCheckInterval = setInterval(() => {
if (lastQueryFinished) {
lastQueryFinished = false;
this.query("SELECT name FROM osu_info LIMIT 1")
.then(data => {
if (!this.connected) {
this.connected = true;
ConsoleHelper.printInfo(`Connected to database. Took ${Date.now() - classCreationTime}ms`);
clearInterval(connectionCheckInterval);
lastQueryFinished = true;
connectedCallback();
}
})
.catch(err => {
lastQueryFinished = true;
});
}
}, 16);
} }
public query(query = "", data?:Array<any>) { public query(query = "", data?:Array<any>) {

View file

@ -19,6 +19,8 @@ export class LoginInfo {
data = data.toString(); data = data.toString();
} }
console.log(data);
const loginData:Array<string> = data.split("\n"); const loginData:Array<string> = data.split("\n");
const extraData:Array<string> = loginData[2].split("|"); const extraData:Array<string> = loginData[2].split("|");

View file

@ -1,5 +1,5 @@
import { Channel } from "./Channel"; import { Channel } from "./Channel";
import { SharedContent } from "../interfaces/SharedContent"; import { Shared } from "../objects/Shared";
import { DataStream } from "./DataStream"; import { DataStream } from "./DataStream";
import { Slot } from "./Slot"; import { Slot } from "./Slot";
import { User } from "./User"; import { User } from "./User";
@ -11,8 +11,7 @@ import { MatchStartSkipData } from "../interfaces/MatchStartSkipData";
import { Mods } from "../enums/Mods"; import { Mods } from "../enums/Mods";
import { PlayerScore } from "../interfaces/PlayerScore"; import { PlayerScore } from "../interfaces/PlayerScore";
import { MatchScoreData } from "../interfaces/MatchScoreData"; import { MatchScoreData } from "../interfaces/MatchScoreData";
import { osu } from "../../osuTyping";
const osu = require("osu-packet");
export class Match { export class Match {
// osu! Data // osu! Data
@ -44,11 +43,14 @@ export class Match {
public playerScores?:Array<PlayerScore>; public playerScores?:Array<PlayerScore>;
private cachedMatchJSON:MatchData; public countdownTime:number = 0;
private readonly sharedContent:SharedContent; public countdownTimer?:NodeJS.Timeout;
private constructor(matchData:MatchData, sharedContent:SharedContent) { private cachedMatchJSON:MatchData;
this.sharedContent = sharedContent; private readonly shared:Shared;
private constructor(matchData:MatchData, shared:Shared) {
this.shared = shared;
this.matchId = matchData.matchId; this.matchId = matchData.matchId;
this.inProgress = matchData.inProgress; this.inProgress = matchData.inProgress;
@ -70,11 +72,11 @@ export class Match {
if (slot.playerId === -1) { if (slot.playerId === -1) {
this.slots.push(new Slot(i, slot.status, slot.team, undefined, slot.mods)); this.slots.push(new Slot(i, slot.status, slot.team, undefined, slot.mods));
} else { } else {
this.slots.push(new Slot(i, slot.status, slot.team, sharedContent.users.getById(slot.playerId), slot.mods)); this.slots.push(new Slot(i, slot.status, slot.team, shared.users.getById(slot.playerId), slot.mods));
} }
} }
const hostUser = sharedContent.users.getById(matchData.host); const hostUser = shared.users.getById(matchData.host);
if (hostUser === undefined) { if (hostUser === undefined) {
// NOTE: This should never be possible to hit // NOTE: This should never be possible to hit
// since this user JUST made the match. // since this user JUST made the match.
@ -90,39 +92,37 @@ export class Match {
this.seed = matchData.seed; this.seed = matchData.seed;
this.matchStream = sharedContent.streams.CreateStream(`multiplayer:match_${this.matchId}`, false); this.matchStream = shared.streams.CreateStream(`multiplayer:match_${this.matchId}`, false);
this.matchChatChannel = sharedContent.chatManager.AddSpecialChatChannel("multiplayer", `mp_${this.matchId}`); this.matchChatChannel = shared.chatManager.AddSpecialChatChannel("multiplayer", `mp_${this.matchId}`);
this.cachedMatchJSON = matchData; this.cachedMatchJSON = matchData;
//this.playerScores = null;
//this.multiplayerExtras = null; //this.multiplayerExtras = null;
//this.isTourneyMatch = false; //this.isTourneyMatch = false;
//this.tourneyClientUsers = []; //this.tourneyClientUsers = [];
} }
public static createMatch(matchHost:User, matchData:MatchData, sharedContent:SharedContent) : Promise<Match> { public static createMatch(matchHost:User, matchData:MatchData, shared:Shared) : Promise<Match> {
return new Promise<Match>(async (resolve, reject) => { return new Promise<Match>(async (resolve, reject) => {
try { try {
matchData.matchId = (await sharedContent.database.query( matchData.matchId = (await shared.database.query(
"INSERT INTO mp_matches (id, name, open_time, close_time, seed) VALUES (NULL, ?, UNIX_TIMESTAMP(), NULL, ?) RETURNING id;", "INSERT INTO mp_matches (id, name, open_time, close_time, seed) VALUES (NULL, ?, UNIX_TIMESTAMP(), NULL, ?) RETURNING id;",
[matchData.gameName, matchData.seed] [matchData.gameName, matchData.seed]
))[0]["id"]; ))[0]["id"];
const matchInstance = new Match(matchData, sharedContent); const matchInstance = new Match(matchData, shared);
// Update the status of the current user // Update the status of the current user
StatusUpdate(matchHost, matchHost.id); StatusUpdate(matchHost, matchHost.id);
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchNew(matchInstance.generateMatchJSON()); osuPacketWriter.MatchNew(matchInstance.generateMatchJSON());
matchHost.addActionToQueue(osuPacketWriter.toBuffer); matchHost.addActionToQueue(osuPacketWriter.toBuffer);
sharedContent.mutiplayerManager.UpdateLobbyListing(); shared.multiplayerManager.UpdateLobbyListing();
resolve(matchInstance); resolve(matchInstance);
} catch (e) { } catch (e) {
@ -166,7 +166,7 @@ export class Match {
this.matchStream.RemoveUser(user); this.matchStream.RemoveUser(user);
this.matchChatChannel.Leave(user); this.matchChatChannel.Leave(user);
// Send this after removing the user from match streams to avoid a leave notification for self // Send this after removing the user from match streams to avoid a leave notification for self?
this.sendMatchUpdate(); this.sendMatchUpdate();
} }
@ -192,7 +192,7 @@ export class Match {
this.beatmapChecksum = this.cachedMatchJSON.beatmapChecksum = matchData.beatmapChecksum; this.beatmapChecksum = this.cachedMatchJSON.beatmapChecksum = matchData.beatmapChecksum;
if (matchData.host !== this.host.id) { if (matchData.host !== this.host.id) {
const hostUser = this.sharedContent.users.getById(matchData.host); const hostUser = this.shared.users.getById(matchData.host);
if (hostUser === undefined) { if (hostUser === undefined) {
// NOTE: This should never be possible to hit // NOTE: This should never be possible to hit
throw "Host User of match was undefined"; throw "Host User of match was undefined";
@ -220,22 +220,24 @@ export class Match {
} }
queryData.push(this.matchId); queryData.push(this.matchId);
await this.sharedContent.database.query(`UPDATE mp_matches SET ${gameNameChanged ? `name = ?${gameSeedChanged ? ", " : ""}` : ""}${gameSeedChanged ? `seed = ?` : ""} WHERE id = ?`, queryData); await this.shared.database.query(`UPDATE mp_matches SET ${gameNameChanged ? `name = ?${gameSeedChanged ? ", " : ""}` : ""}${gameSeedChanged ? `seed = ?` : ""} WHERE id = ?`, queryData);
} }
this.sendMatchUpdate(); this.sendMatchUpdate();
// Update the match listing in the lobby to reflect these changes // Update the match listing in the lobby to reflect these changes
this.sharedContent.mutiplayerManager.UpdateLobbyListing(); this.shared.multiplayerManager.UpdateLobbyListing();
} }
public sendMatchUpdate() { public sendMatchUpdate() {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchUpdate(this.generateMatchJSON()); osuPacketWriter.MatchUpdate(this.generateMatchJSON());
// Update all users in the match with new match information // Update all users in the match with new match information
this.matchStream.Send(osuPacketWriter.toBuffer); this.matchStream.Send(osuPacketWriter.toBuffer);
console.log(this.slots);
} }
public moveToSlot(user:User, slotToMoveTo:number) { public moveToSlot(user:User, slotToMoveTo:number) {
@ -252,7 +254,7 @@ export class Match {
this.sendMatchUpdate(); this.sendMatchUpdate();
this.sharedContent.mutiplayerManager.UpdateLobbyListing(); this.shared.multiplayerManager.UpdateLobbyListing();
} }
public changeTeam(user:User) { public changeTeam(user:User) {
@ -325,7 +327,7 @@ export class Match {
this.sendMatchUpdate(); this.sendMatchUpdate();
} }
this.sharedContent.mutiplayerManager.UpdateLobbyListing(); this.shared.multiplayerManager.UpdateLobbyListing();
} }
public missingBeatmap(user:User) { public missingBeatmap(user:User) {
@ -383,7 +385,7 @@ export class Match {
// All players have finished playing, finish the match // All players have finished playing, finish the match
if (allSkipped) { if (allSkipped) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchPlayerSkipped(user.id); osuPacketWriter.MatchPlayerSkipped(user.id);
osuPacketWriter.MatchSkip(); osuPacketWriter.MatchSkip();
@ -392,7 +394,7 @@ export class Match {
this.matchSkippedSlots = undefined; this.matchSkippedSlots = undefined;
} else { } else {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchPlayerSkipped(user.id); osuPacketWriter.MatchPlayerSkipped(user.id);
@ -433,7 +435,7 @@ export class Match {
this.sendMatchUpdate(); this.sendMatchUpdate();
} }
this.sharedContent.mutiplayerManager.UpdateLobbyListing(); this.shared.multiplayerManager.UpdateLobbyListing();
} }
startMatch() { startMatch() {
@ -446,6 +448,7 @@ export class Match {
this.matchLoadSlots = new Array<MatchStartSkipData>(); this.matchLoadSlots = new Array<MatchStartSkipData>();
// Loop through all slots in the match // Loop through all slots in the match
console.log(this.slots);
for (let slot of this.slots) { for (let slot of this.slots) {
// Make sure the slot has a user in it // Make sure the slot has a user in it
if (slot.player === undefined || slot.status === SlotStatus.Empty || slot.status === SlotStatus.Locked) { if (slot.player === undefined || slot.status === SlotStatus.Empty || slot.status === SlotStatus.Locked) {
@ -462,7 +465,7 @@ export class Match {
slot.status = 32; slot.status = 32;
} }
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchStart(this.generateMatchJSON()); osuPacketWriter.MatchStart(this.generateMatchJSON());
@ -473,7 +476,7 @@ export class Match {
this.sendMatchUpdate(); this.sendMatchUpdate();
// Update match listing in lobby to show the game is in progress // Update match listing in lobby to show the game is in progress
this.sharedContent.mutiplayerManager.UpdateLobbyListing(); this.shared.multiplayerManager.UpdateLobbyListing();
} }
public matchPlayerLoaded(user:User) { public matchPlayerLoaded(user:User) {
@ -494,7 +497,7 @@ export class Match {
// All players have loaded the beatmap, start playing. // All players have loaded the beatmap, start playing.
if (allLoaded) { if (allLoaded) {
let osuPacketWriter = new osu.Bancho.Writer; let osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchAllPlayersLoaded(); osuPacketWriter.MatchAllPlayersLoaded();
this.matchStream.Send(osuPacketWriter.toBuffer); this.matchStream.Send(osuPacketWriter.toBuffer);
@ -563,7 +566,7 @@ export class Match {
this.matchLoadSlots = undefined; this.matchLoadSlots = undefined;
this.inProgress = false; this.inProgress = false;
let osuPacketWriter = new osu.Bancho.Writer; let osuPacketWriter = osu.Bancho.Writer();
let queryData:Array<any> = [ let queryData:Array<any> = [
this.matchId, this.matchId,
@ -599,7 +602,7 @@ export class Match {
slot.status = SlotStatus.NotReady; slot.status = SlotStatus.NotReady;
} }
await this.sharedContent.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.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);
osuPacketWriter.MatchComplete(); osuPacketWriter.MatchComplete();
@ -608,7 +611,7 @@ export class Match {
// Update all users in the match with new info // Update all users in the match with new info
this.sendMatchUpdate(); this.sendMatchUpdate();
this.sharedContent.mutiplayerManager.UpdateLobbyListing(); this.shared.multiplayerManager.UpdateLobbyListing();
// TODO: Re-implement multiplayer extras // TODO: Re-implement multiplayer extras
//if (this.multiplayerExtras != null) this.multiplayerExtras.onMatchFinished(JSON.parse(JSON.stringify(this.playerScores))); //if (this.multiplayerExtras != null) this.multiplayerExtras.onMatchFinished(JSON.parse(JSON.stringify(this.playerScores)));
@ -617,7 +620,7 @@ export class Match {
} }
updatePlayerScore(user:User, matchScoreData:MatchScoreData) { updatePlayerScore(user:User, matchScoreData:MatchScoreData) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
if (user.matchSlot === undefined || user.matchSlot.player === undefined || this.playerScores === undefined) { if (user.matchSlot === undefined || user.matchSlot.player === undefined || this.playerScores === undefined) {
return; return;
@ -647,7 +650,7 @@ export class Match {
} }
matchFailed(user:User) { matchFailed(user:User) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
// Make sure the user is in the match in a valid slot // Make sure the user is in the match in a valid slot
if (user.matchSlot === undefined) { if (user.matchSlot === undefined) {

View file

@ -4,7 +4,7 @@ import { Match } from "./Match";
export class MatchArray extends FunkyArray<Match> { export class MatchArray extends FunkyArray<Match> {
public getById(id:number) : Match | undefined { public getById(id:number) : Match | undefined {
for (let match of this.getIterableItems()) { for (let match of this.getIterableItems()) {
if (match.matchId == id) { if (match.matchId === id) {
return match; return match;
} }
} }

View file

@ -0,0 +1,61 @@
import { osu } from "../../osuTyping";
import { Shared } from "../objects/Shared";
import { Channel } from "./Channel";
import { DataStream } from "./DataStream";
import { User } from "./User";
export class PrivateChannel extends Channel {
private readonly user0:User;
private readonly user1:User;
public constructor(user0:User, user1:User, stream:DataStream) {
super(user0.shared, `${user0.username}${user1.username}`, "", stream);
this.user0 = user0;
this.user1 = user1;
}
public override SendMessage(sender:User, message:string) {
const osuPacketWriter = osu.Bancho.Writer();
if (!this.stream.HasUser(this.user0)) {
this.Join(this.user0);
}
if (!this.stream.HasUser(this.user1)) {
this.Join(this.user1);
}
let target:string = this.user1.username;
if (sender.uuid === this.user1.uuid) {
target = this.user0.username;
}
osuPacketWriter.SendMessage({
sendingClient: sender.username,
message: message,
target: target,
senderId: sender.id
});
this.stream.SendWithExclusion(osuPacketWriter.toBuffer, sender);
}
public override Join(user:User) {
this.stream.AddUser(user);
const osuPacketWriter = osu.Bancho.Writer();
if (user.uuid === this.user0.uuid) {
osuPacketWriter.ChannelJoinSuccess(this.user1.username);
} else if (user.uuid === this.user1.uuid) {
osuPacketWriter.ChannelJoinSuccess(this.user0.username);
}
user.addActionToQueue(osuPacketWriter.toBuffer);
}
public override Leave(user:User) {
this.stream.RemoveUser(user);
const osuPacketWriter = osu.Bancho.Writer();
if (user.id === this.user0.id) {
osuPacketWriter.ChannelRevoked(this.user1.username);
} else if (user.id === this.user1.id) {
osuPacketWriter.ChannelRevoked(this.user0.username);
}
user.addActionToQueue(osuPacketWriter.toBuffer);
}
}

44
server/objects/Shared.ts Normal file
View file

@ -0,0 +1,44 @@
import { ChatManager } from "../ChatManager";
import { Config } from "../interfaces/Config";
import { Database } from "../objects/Database";
import { DataStreamArray } from "../objects/DataStreamArray";
import { MultiplayerManager } from "../MultiplayerManager";
import { PrivateChatManager } from "../PrivateChatManager";
import { readFileSync } from "fs";
import { UserArray } from "../objects/UserArray";
import { User } from "./User";
import { LatLng } from "./LatLng";
import { Bot } from "../Bot";
export class Shared {
public readonly chatManager:ChatManager;
public readonly config:Config;
public readonly database:Database;
public readonly multiplayerManager:MultiplayerManager;
public readonly privateChatManager:PrivateChatManager;
public readonly streams:DataStreamArray;
public readonly users:UserArray;
public readonly bot:Bot;
public constructor() {
this.config = JSON.parse(readFileSync("./config.json").toString()) as Config;
this.database = new Database(this.config.database.address, this.config.database.port, this.config.database.username, this.config.database.password, this.config.database.name);
this.streams = new DataStreamArray();
// Add the bot user
this.users = new UserArray();
const botUser = this.users.add("bot", new User(3, "SillyBot", "bot", this));
botUser.location = new LatLng(50, -32);
this.bot = new Bot(this, botUser);
this.chatManager = new ChatManager(this);
// Setup chat channels
this.chatManager.AddChatChannel("osu", "The main channel", true);
this.chatManager.AddChatChannel("lobby", "Talk about multiplayer stuff");
this.chatManager.AddChatChannel("english", "Talk in exclusively English");
this.chatManager.AddChatChannel("japanese", "Talk in exclusively Japanese");
this.multiplayerManager = new MultiplayerManager(this);
this.privateChatManager = new PrivateChatManager(this);
}
}

View file

@ -3,7 +3,7 @@ import { RankingModes } from "../enums/RankingModes";
import { Match } from "./Match"; import { Match } from "./Match";
import { DataStream } from "./DataStream"; import { DataStream } from "./DataStream";
import { StatusUpdate } from "../packets/StatusUpdate"; import { StatusUpdate } from "../packets/StatusUpdate";
import { SharedContent } from "../interfaces/SharedContent"; import { Shared } from "../objects/Shared";
import { Slot } from "./Slot"; import { Slot } from "./Slot";
const rankingModes = [ const rankingModes = [
@ -15,7 +15,7 @@ const rankingModes = [
export class User { export class User {
private static readonly EMPTY_BUFFER = Buffer.alloc(0); private static readonly EMPTY_BUFFER = Buffer.alloc(0);
public sharedContent:SharedContent; public shared:Shared;
public id:number; public id:number;
public username:string; public username:string;
@ -65,12 +65,12 @@ export class User {
return user0.uuid === user1.uuid; return user0.uuid === user1.uuid;
} }
public constructor(id:number, username:string, uuid:string, sharedContent:SharedContent) { public constructor(id:number, username:string, uuid:string, shared:Shared) {
this.id = id; this.id = id;
this.username = username; this.username = username;
this.uuid = uuid; this.uuid = uuid;
this.sharedContent = sharedContent; this.shared = shared;
} }
// Concats new actions to the user's queue // Concats new actions to the user's queue
@ -83,7 +83,7 @@ export class User {
} }
// Updates the user's current action // Updates the user's current action
updatePresence(action:any) : void { updatePresence(action:any) {
this.actionID = action.status; this.actionID = action.status;
this.actionText = action.statusText; this.actionText = action.statusText;
this.beatmapChecksum = action.beatmapChecksum; this.beatmapChecksum = action.beatmapChecksum;
@ -97,10 +97,10 @@ export 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) : Promise<void> { async updateUserInfo(forceUpdate:boolean = false) {
const userScoreDB = await this.sharedContent.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.query("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.sharedContent.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]);
if (userScoreDB == null || userRankDB == null) throw "fuck"; if (userScoreDB == null || userRankDB == null) throw "fuck";

View file

@ -0,0 +1,23 @@
export class UserInfo {
id:number = Number.MIN_VALUE;
username:string = "";
username_safe:string = "";
password_hash:string = "";
password_salt:string = "";
email:string = "";
country:string = "";
reg_date:Date = new Date(0);
last_login_date:Date = new Date(0);
last_played_mode:number = Number.MIN_VALUE;
online_now:boolean = false;
tags:number = Number.MIN_VALUE;
supporter:boolean = false;
web_session:string = "";
verification_needed:boolean = false;
password_change_required:boolean = false;
has_old_password:number = Number.MIN_VALUE;
password_reset_key:string = "";
away_message:string = "";
last_modified_time:Date = new Date(0);
is_deleted:boolean = false;
}

View file

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

View file

@ -5,7 +5,9 @@ export function ChangeAction(user:User, data:any) {
user.updatePresence(data); user.updatePresence(data);
if (user.spectatorStream != null) { if (user.spectatorStream != null) {
const statusUpdate = StatusUpdate(user.sharedContent, user.id); const statusUpdate = StatusUpdate(user.shared, user.id);
user.spectatorStream.Send(statusUpdate); if (statusUpdate instanceof Buffer) {
user.spectatorStream.Send(statusUpdate);
}
} }
} }

View file

@ -8,12 +8,12 @@ export async function Logout(user:User) {
const logoutStartTime = Date.now(); const logoutStartTime = Date.now();
user.sharedContent.streams.RemoveUserFromAllStreams(user); user.shared.streams.RemoveUserFromAllStreams(user);
// Remove user from user list // Remove user from user list
user.sharedContent.users.remove(user.uuid); user.shared.users.remove(user.uuid);
await user.sharedContent.database.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [user.sharedContent.users.getLength() - 1]); await user.shared.database.query("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

@ -0,0 +1,27 @@
import { MessageData } from "../interfaces/MessageData";
import { Shared } from "../objects/Shared";
import { PrivateChannel } from "../objects/PrivateChannel";
import { User } from "../objects/User";
export function PrivateMessage(user:User, message:MessageData) {
const shared:Shared = user.shared;
const sendingTo = shared.users.getByUsername(message.target);
if (!(sendingTo instanceof User)) {
console.log("Sending User invalid");
return;
}
let channel = shared.privateChatManager.GetChannelByName(`${user.username}${sendingTo.username}`);
if (!(channel instanceof PrivateChannel)) {
console.log("First find failed");
// Try it the other way around
channel = shared.privateChatManager.GetChannelByName(`${sendingTo.username}${user.username}`);
}
if (!(channel instanceof PrivateChannel)) {
console.log("Second find failed, creating");
channel = shared.privateChatManager.AddChannel(user, sendingTo);
}
console.log("sending");
channel.SendMessage(user, message.message);
}

View file

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

View file

@ -1,22 +1,22 @@
import { SharedContent } from "../interfaces/SharedContent"; import { Shared } from "../objects/Shared";
import { RankingModes } from "../enums/RankingModes"; import { RankingModes } from "../enums/RankingModes";
import { User } from "../objects/User"; import { User } from "../objects/User";
const osu = require("osu-packet"); import { osu } from "../../osuTyping";
export function StatusUpdate(arg0:User | SharedContent, id:number) { export function StatusUpdate(arg0:User | Shared, id:number) {
if (id == 3) return; // Ignore Bot if (id == 3) return; // Ignore Bot
// Create new osu packet writer // Create new osu packet writer
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
let sharedContent:SharedContent; let shared:Shared;
if (arg0 instanceof User) { if (arg0 instanceof User) {
sharedContent = arg0.sharedContent; shared = arg0.shared;
} else { } else {
sharedContent = arg0; shared = arg0;
} }
// Get user's class // Get user's class
const userData = sharedContent.users.getById(id); const userData = shared.users.getById(id);
if (userData == null) return; if (userData == null) return;

View file

@ -0,0 +1,11 @@
import { osu } from "../../osuTyping";
import { User } from "../objects/User";
export function TourneyMatchJoinChannel(user:User, matchId:number) {
const match = user.shared.multiplayerManager.GetMatchById(matchId);
if (match === undefined) {
return;
}
match.matchChatChannel.Join(user);
}

View file

@ -0,0 +1,10 @@
import { User } from "../objects/User";
export function TourneyMatchLeaveChannel(user:User, matchId:number) {
const match = user.shared.multiplayerManager.GetMatchById(matchId);
if (match === undefined) {
return;
}
match.matchChatChannel.Leave(user);
}

View file

@ -0,0 +1,31 @@
import { osu } from "../../osuTyping";
import { Match } from "../objects/Match";
import { User } from "../objects/User";
import { StatusUpdate } from "./StatusUpdate";
import { UserPresence } from "./UserPresence";
export function TourneyMatchSpecialInfo(user:User, matchId:number) {
const match = user.shared.multiplayerManager.GetMatchById(matchId);
if (!(match instanceof Match)) {
return;
}
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchUpdate(match.generateMatchJSON());
for (const slot of match.slots) {
if (slot.player === undefined) {
continue;
}
const presenceBuffer = UserPresence(user, slot.player.id);
const statusBuffer = StatusUpdate(user, slot.player.id);
if (presenceBuffer instanceof Buffer && statusBuffer instanceof Buffer) {
user.addActionToQueue(presenceBuffer);
user.addActionToQueue(statusBuffer);
}
user.addActionToQueue(osuPacketWriter.toBuffer);
}
}

View file

@ -1,17 +1,17 @@
import { SharedContent } from "../interfaces/SharedContent"; import { osu } from "../../osuTyping";
import { Shared } from "../objects/Shared";
import { User } from "../objects/User"; import { User } from "../objects/User";
const osu = require("osu-packet");
export function UserPresence(arg0:User | SharedContent, id:number) { export function UserPresence(arg0:User | Shared, id:number) {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
let sharedContent:SharedContent; let shared:Shared;
if (arg0 instanceof User) { if (arg0 instanceof User) {
sharedContent = arg0.sharedContent; shared = arg0.shared;
} else { } else {
sharedContent = arg0; shared = arg0;
} }
const userData = sharedContent.users.getById(id); const userData = shared.users.getById(id);
if (userData == null) return; if (userData == null) return;

View file

@ -1,19 +1,19 @@
import { SharedContent } from "../interfaces/SharedContent"; import { osu } from "../../osuTyping";
import { Shared } from "../objects/Shared";
import { User } from "../objects/User"; import { User } from "../objects/User";
const osu = require("osu-packet");
export function UserPresenceBundle(arg0:User | SharedContent) : Buffer { export function UserPresenceBundle(arg0:User | Shared) : Buffer {
const osuPacketWriter = new osu.Bancho.Writer; const osuPacketWriter = osu.Bancho.Writer();
let sharedContent:SharedContent; let shared:Shared;
if (arg0 instanceof User) { if (arg0 instanceof User) {
sharedContent = arg0.sharedContent; shared = arg0.shared;
} else { } else {
sharedContent = arg0; shared = arg0;
} }
let userIds:Array<number> = new Array<number>(); let userIds:Array<number> = new Array<number>();
for (let userData of sharedContent.users.getIterableItems()) { for (let userData of shared.users.getIterableItems()) {
userIds.push(userData.id); userIds.push(userData.id);
} }

View file

@ -7,6 +7,9 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"rootDir": "./", "rootDir": "./",
"outDir": "./build", "outDir": "./build",
"strict": true "strict": true,
"lib": [
"ES2021.String"
]
} }
} }