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 compression from "compression";
import express from "express";
import { HandleRequest, GetSharedContent } from "./server/BanchoServer";
import { SharedContent } from "./server/interfaces/SharedContent";
import { HandleRequest } from "./server/BanchoServer";
import { Shared } from "./server/objects/Shared";
import { Registry, collectDefaultMetrics } from "prom-client";
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();
@ -55,6 +53,7 @@ binatoApp.use((req, res) => {
if (req.url == "/" || req.url == "/index.html" || req.url == "/index") {
res.send(INDEX_PAGE);
} else if (req.url == "/chat") {
// I don't think this works??
res.send(ChatHistory.GenerateForWeb());
}
break;

View file

@ -1,4 +1,4 @@
import chalk from "chalk";
import * as dyetty from "dyetty";
enum LogType {
INFO,
@ -7,14 +7,14 @@ enum LogType {
};
const LogTags = {
INFO: chalk.bgGreen(chalk.black(" INFO ")),
BANCHO: chalk.bgMagenta(chalk.black(" BANCHO ")),
WEBREQ: chalk.bgGreen(chalk.black(" WEBREQ ")),
CHAT: chalk.bgCyan(chalk.black(" CHAT ")),
WARN: chalk.bgYellow(chalk.black(" WARN ")),
ERROR: chalk.bgRed(" ERRR "),
REDIS: chalk.bgRed(chalk.white(" bREDIS ")),
STREAM: chalk.bgBlue(chalk.black(" STREAM "))
INFO: dyetty.bgGreen(dyetty.black(" INFO ")),
BANCHO: dyetty.bgMagenta(dyetty.black(" BANCHO ")),
WEBREQ: dyetty.bgGreen(dyetty.black(" WEBREQ ")),
CHAT: dyetty.bgCyan(dyetty.black(" CHAT ")),
WARN: dyetty.bgYellow(dyetty.black(" WARN ")),
ERROR: dyetty.bgRed(" ERRR "),
REDIS: dyetty.bgRed(dyetty.white(" bREDIS ")),
STREAM: dyetty.bgBlue(dyetty.black(" STREAM "))
} as const;
function correctValue(i:number) : string {
@ -24,7 +24,7 @@ function correctValue(i:number) : string {
function getTime() : string {
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 {

View file

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

View file

@ -1,58 +1,22 @@
import { Config } from "./interfaces/Config";
import { ConsoleHelper } from "../ConsoleHelper";
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 { LoginProcess } from "./LoginProcess";
import { Packets } from "./enums/Packets";
import { replaceAll } from "./Util";
import { readFileSync } from "fs";
import { RedisClientType, createClient } from "redis";
import { Request, Response } from "express";
import { SpectatorManager } from "./SpectatorManager";
import { UserArray } from "./objects/UserArray";
import { User } from "./objects/User";
import { MultiplayerManager } from "./MultiplayerManager";
import { SharedContent } from "./interfaces/SharedContent";
const config:Config = JSON.parse(readFileSync("./config.json").toString()) as Config;
// TODO: Port osu-packet to TypeScript
const osu = require("osu-packet");
import { PrivateMessage } from "./packets/PrivateMessage";
import { MessageData } from "./interfaces/MessageData";
import { Shared } from "./objects/Shared";
const sharedContent:any = {};
// NOTE: This function should only be used externaly in Binato.ts and in this file.
export function GetSharedContent() : SharedContent {
return sharedContent;
}
const shared:Shared = new Shared();
shared.database.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL");
shared.database.query("UPDATE osu_info SET value = 0 WHERE name = 'online_now'");
const DB:Database = sharedContent.database = new Database(config.database.address, config.database.port, config.database.username, config.database.password, config.database.name, async () => {
// Close any unclosed db matches on startup
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());
// Server Setup
const spectatorManager:SpectatorManager = new SpectatorManager(shared);
let redisClient:RedisClientType;
@ -65,10 +29,10 @@ async function subscribeToChannel(channelName:string, callback:(message:string)
ConsoleHelper.printRedis(`Subscribed to ${channelName} channel`);
}
if (config.redis.enabled) {
if (shared.config.redis.enabled) {
(async () => {
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));
@ -79,14 +43,12 @@ if (config.redis.enabled) {
// Score submit update channel
subscribeToChannel("binato:update_user_stats", (message) => {
if (typeof(message) === "string") {
const user = users.getById(parseInt(message));
if (user != null) {
// Update user info
user.updateUserInfo(true);
const user = shared.users.getById(parseInt(message));
if (user != null) {
// Update user info
user.updateUserInfo(true);
ConsoleHelper.printRedis(`Score submission stats update request received for ${user.username}`);
}
ConsoleHelper.printRedis(`Score submission stats update request received for ${user.username}`);
}
});
})();
@ -98,10 +60,18 @@ import { Logout } from "./packets/Logout";
import { UserPresence } from "./packets/UserPresence";
import { UserStatsRequest } from "./packets/UserStatsRequest";
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
setInterval(() => {
for (let User of users.getIterableItems()) {
for (let User of shared.users.getIterableItems()) {
if (User.uuid == "bot") continue; // Ignore the bot
// Logout this user, they're clearly gone.
@ -114,29 +84,29 @@ setInterval(() => {
const EMPTY_BUFFER = Buffer.alloc(0);
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
const requestTokenString:string | undefined = req.header("osu-token");
// Check if the user is logged in
if (requestTokenString == null) {
// 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!
await LoginProcess(req, res, packet, GetSharedContent());
DB.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [users.getLength() - 1]);
await LoginProcess(req, res, packet, shared);
shared.database.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [shared.users.getLength() - 1]);
}
} else {
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.
try {
// 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
if (PacketUser != null) {
@ -144,7 +114,7 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
PacketUser.timeoutTime = Date.now() + 60000;
// Create a new osu! packet reader
const osuPacketReader = new osu.Client.Reader(packet);
const osuPacketReader = osu.Client.Reader(packet);
// Parse current bancho packet
const PacketData = osuPacketReader.Parse();
@ -156,7 +126,8 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break;
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) {
channel.SendMessage(PacketUser, CurrentPacket.data.message);
}
@ -183,23 +154,23 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break;
case Packets.Client_SendPrivateMessage:
//SendPrivateMessage(PacketUser, CurrentPacket.data);
PrivateMessage(PacketUser, CurrentPacket.data);
break;
case Packets.Client_JoinLobby:
multiplayerManager.JoinLobby(PacketUser);
shared.multiplayerManager.JoinLobby(PacketUser);
break;
case Packets.Client_PartLobby:
multiplayerManager.LeaveLobby(PacketUser);
shared.multiplayerManager.LeaveLobby(PacketUser);
break;
case Packets.Client_CreateMatch:
await multiplayerManager.CreateMatch(PacketUser, CurrentPacket.data);
await shared.multiplayerManager.CreateMatch(PacketUser, CurrentPacket.data);
break;
case Packets.Client_JoinMatch:
multiplayerManager.JoinMatch(PacketUser, CurrentPacket.data);
shared.multiplayerManager.JoinMatch(PacketUser, CurrentPacket.data);
break;
case Packets.Client_MatchChangeSlot:
@ -284,11 +255,11 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break;
case Packets.Client_FriendAdd:
//AddFriend(PacketUser, CurrentPacket.data);
AddFriend(PacketUser, CurrentPacket.data);
break;
case Packets.Client_FriendRemove:
//RemoveFriend(PacketUser, CurrentPacket.data);
RemoveFriend(PacketUser, CurrentPacket.data);
break;
case Packets.Client_UserStatsRequest:
@ -296,15 +267,15 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
break;
case Packets.Client_SpecialMatchInfoRequest:
//TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data);
TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data);
break;
case Packets.Client_SpecialJoinMatchChannel:
//TourneyMatchJoinChannel(PacketUser, CurrentPacket.data);
TourneyMatchJoinChannel(PacketUser, CurrentPacket.data);
break;
case Packets.Client_SpecialLeaveMatchChannel:
//TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data);
TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data);
break;
case Packets.Client_Invite:
@ -328,14 +299,18 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
PacketUser.clearQueue();
} else {
// 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
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...";
}
}
} catch (e) {
console.error(e);
if (Constants.DEBUG) {
throw e;
}
ConsoleHelper.printError(`${e}`);
} finally {
res.writeHead(200, {
"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 { FunkyArray } from "./objects/FunkyArray";
import { User } from "./objects/User";
import { SharedContent } from "./interfaces/SharedContent";
const osu = require("osu-packet");
import { Shared } from "./objects/Shared";
import { osu } from "../osuTyping";
import { PrivateChannel } from "./objects/PrivateChannel";
export class ChatManager {
public chatChannels:FunkyArray<Channel> = new FunkyArray<Channel>();
public forceJoinChannels:FunkyArray<Channel> = new FunkyArray<Channel>();
private readonly sharedContent:SharedContent;
private readonly shared:Shared;
public constructor(sharedContent:SharedContent) {
this.sharedContent = sharedContent;
public constructor(shared:Shared) {
this.shared = shared;
}
public AddChatChannel(name:string, description:string, forceJoin:boolean = false) : Channel {
const stream = this.sharedContent.streams.CreateStream(`chat_channel:${name}`, false);
const channel = new Channel(this.sharedContent, `#${name}`, description, stream);
const stream = this.shared.streams.CreateStream(`chat_channel:${name}`, false);
const channel = new Channel(this.shared, `#${name}`, description, stream);
this.chatChannels.add(channel.name, channel);
if (forceJoin) {
this.forceJoinChannels.add(name, channel);
@ -26,8 +27,8 @@ export class ChatManager {
}
public AddSpecialChatChannel(name:string, streamName:string, forceJoin:boolean = false) : Channel {
const stream = this.sharedContent.streams.CreateStream(`chat_channel:${streamName}`, false);
const channel = new Channel(this.sharedContent, `#${name}`, "", stream);
const stream = this.shared.streams.CreateStream(`chat_channel:${streamName}`, false);
const channel = new Channel(this.shared, `#${name}`, "", stream);
this.chatChannels.add(channel.name, channel);
if (forceJoin) {
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 {
return this.chatChannels.getByKey(channelName);
}
public GetPrivateChannelByName(channelName:string) : Channel | undefined {
return this.chatChannels.getByKey(channelName);
}
public ForceJoinChannels(user:User) {
for (let channel of this.forceJoinChannels.getIterableItems()) {
channel.Join(user);
}
}
public SendChannelListing(user:User) {
const osuPacketWriter = new osu.Bancho.Writer;
const osuPacketWriter = osu.Bancho.Writer();
for (let channel of this.chatChannels.getIterableItems()) {
if (channel.isSpecial) {
continue;

View file

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

View file

@ -1,5 +1,4 @@
import { ConsoleHelper } from "../ConsoleHelper";
import { Database } from "./objects/Database";
import fetch from "node-fetch";
import { getCountryID } from "./Country";
import { generateSession } from "./Util";
@ -7,46 +6,21 @@ import { LatLng } from "./objects/LatLng";
import { LoginInfo } from "./objects/LoginInfo";
import { Logout } from "./packets/Logout";
import { pbkdf2 } from "crypto";
import { readFileSync } from "fs";
import { Request, Response } from "express";
import { UserArray } from "./objects/UserArray";
import { User } from "./objects/User";
import { DataStreamArray } from "./objects/DataStreamArray";
import { ChatManager } from "./ChatManager";
import { UserPresenceBundle } from "./packets/UserPresenceBundle";
import { UserPresence } from "./packets/UserPresence";
import { StatusUpdate } from "./packets/StatusUpdate";
import { SharedContent } from "./interfaces/SharedContent";
const config:any = JSON.parse(readFileSync("./config.json").toString());
import { Shared } from "./objects/Shared";
import { osu } from "../osuTyping";
import { IpZxqResponse } from "./interfaces/IpZxqResponse";
const { decrypt: aesDecrypt } = require("aes256");
const osu = require("osu-packet");
function incorrectLoginResponse() {
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.LoginReply(-1);
return [
osuPacketWriter.toBuffer,
{
'cho-protocol': 19,
'Connection': 'keep-alive',
'Keep-Alive': 'timeout=5, max=100',
}
];
}
const incorrectLoginResponse:Buffer = osu.Bancho.Writer().LoginReply(-1).toBuffer;
function requiredPWChangeResponse() {
const osuPacketWriter = new osu.Bancho.Writer;
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.");
osuPacketWriter.LoginReply(-1);
return [
osuPacketWriter.toBuffer,
{
'cho-protocol': 19,
'Connection': 'keep-alive',
'Keep-Alive': 'timeout=5, max=100',
}
];
}
const requiredPWChangeResponse:Buffer = osu.Bancho.Writer()
.LoginReply(-1)
.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;
enum LoginTypes {
CURRENT,
@ -54,172 +28,210 @@ enum LoginTypes {
OLD_AES
}
function TestLogin(loginInfo:LoginInfo | undefined, database:Database) {
return new Promise(async (resolve, reject) => {
// Check if there is any login information provided
if (loginInfo == null) return resolve(incorrectLoginResponse());
enum LoginResult {
VALID,
MIGRATION,
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
if (userDBData == null) return resolve(incorrectLoginResponse());
if (userDBData == null) return resolve(LoginResult.INCORRECT);
// Make sure the username is the same as the login info
if (userDBData.username !== loginInfo.username) return resolve(incorrectLoginResponse());
/*
1: Old MD5 password
2: Old AES password
*/
if (userDBData.username !== loginInfo.username) return resolve(LoginResult.INCORRECT);
console.log(userDBData.has_old_password);
switch (userDBData.has_old_password) {
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) {
return reject(err);
} else {
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;
case LoginTypes.OLD_AES:
if (aesDecrypt(config.database.key, userDBData.password_hash) !== loginInfo.password) {
return resolve(resolve(incorrectLoginResponse()));
console.log("OLD AES");
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:
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) {
const loginInfo = LoginInfo.From(packet);
export async function LoginProcess(req:Request, res:Response, packet:Buffer, shared:Shared) {
const loginStartTime = Date.now();
const loginInfo = LoginInfo.From(packet);
const loginCheck:any = await TestLogin(loginInfo, sharedContent.database);
if (loginCheck != null) {
res.writeHead(200, loginCheck[1]);
return res.end(loginCheck[0]);
// Send back no data if there's no loginInfo
// Somebody is doing something funky
if (loginInfo === undefined) {
return res.end("");
}
if (loginInfo == null)
return;
const loginResult:LoginResult = await TestLogin(loginInfo, shared);
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 cloudflare requestee IP first
let requestIP = req.get("cf-connecting-ip");
// Get users IP for getting location
// Get cloudflare requestee IP first
let requestIP = req.get("cf-connecting-ip");
// Get IP of requestee since we are probably behind a reverse proxy
if (requestIP == null)
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);
// Get IP of requestee since we are probably behind a reverse proxy
if (requestIP === undefined) {
requestIP = req.get("X-Real-IP");
}
// 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');
res.removeHeader('Date');
// Complete login
// Make sure requestIP is never undefined
if (requestIP === undefined) {
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, {
"cho-token": newUser.uuid,
"Connection": "keep-alive",
"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}]`);
});
} catch (err) {
console.error(err);
}
}

View file

@ -1,5 +1,5 @@
import { Channel } from "./objects/Channel";
import { SharedContent } from "./interfaces/SharedContent";
import { Shared } from "./objects/Shared";
import { SlotStatus } from "./enums/SlotStatus";
import { DataStream } from "./objects/DataStream";
import { Match } from "./objects/Match";
@ -10,18 +10,18 @@ import { UserPresenceBundle } from "./packets/UserPresenceBundle";
import { MatchArray } from "./objects/MatchArray";
import { MatchJoinData } from "./interfaces/MatchJoinData";
import { MatchData } from "./interfaces/MatchData";
const osu = require("osu-packet");
import { osu } from "../osuTyping";
export class MultiplayerManager {
private readonly sharedContent:SharedContent;
private readonly shared:Shared;
private matches:MatchArray = new MatchArray();
private readonly lobbyStream:DataStream;
private readonly lobbyChat:Channel;
public constructor(sharedContent:SharedContent) {
this.sharedContent = sharedContent;
this.lobbyStream = sharedContent.streams.CreateStream("multiplayer:lobby", false);
const channel = this.sharedContent.chatManager.GetChannelByName("#lobby");
public constructor(shared:Shared) {
this.shared = shared;
this.lobbyStream = shared.streams.CreateStream("multiplayer:lobby", false);
const channel = this.shared.chatManager.GetChannelByName("#lobby");
if (channel === undefined) {
throw "Something has gone horribly wrong, the lobby channel does not exist!";
}
@ -84,7 +84,7 @@ export class MultiplayerManager {
match.matchStream.AddUser(user);
match.matchChatChannel.Join(user);
const osuPacketWriter = new osu.Bancho.Writer;
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchJoinSuccess(match.generateMatchJSON());
@ -92,7 +92,7 @@ export class MultiplayerManager {
this.UpdateLobbyListing();
} catch (e) {
const osuPacketWriter = new osu.Bancho.Writer;
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchJoinFail();
@ -107,8 +107,8 @@ export class MultiplayerManager {
}
public GenerateLobbyListing(user?:User) : Buffer {
const osuPacketWriter = new osu.Bancho.Writer;
let bufferToSend = UserPresenceBundle(this.sharedContent);
const osuPacketWriter = osu.Bancho.Writer();
let bufferToSend = UserPresenceBundle(this.shared);
for (let match of this.matches.getIterableItems()) {
for (let slot of match.slots) {
@ -116,8 +116,13 @@ export class MultiplayerManager {
continue;
}
const presenceBuffer = UserPresence(this.sharedContent, slot.player.id);
const statusBuffer = StatusUpdate(this.sharedContent, slot.player.id);
const presenceBuffer = UserPresence(this.shared, 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);
}
@ -134,8 +139,12 @@ export class MultiplayerManager {
return bufferToSend;
}
public GetMatchById(id:number) : Match | undefined {
return this.matches.getById(id);
}
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.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 { SharedContent } from "./interfaces/SharedContent";
import { Shared } from "./objects/Shared";
import { User } from "./objects/User";
const osu = require("osu-packet");
import { osu } from "../osuTyping";
export class SpectatorManager {
private sharedContent:SharedContent;
private shared:Shared;
public constructor(sharedContent:SharedContent) {
this.sharedContent = sharedContent;
public constructor(shared:Shared) {
this.shared = shared;
}
public startSpectating(user:User, userIdToSpectate:number) {
const userToSpectate = this.sharedContent.users.getById(userIdToSpectate);
const userToSpectate = this.shared.users.getById(userIdToSpectate);
if (userToSpectate === undefined) {
return;
}
@ -19,20 +19,20 @@ export class SpectatorManager {
// Use existing or create spectator stream
let spectateStream:DataStream;
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 {
user.spectatorStream = spectateStream = userToSpectate.spectatorStream;
}
user.spectatingUser = userToSpectate;
let osuPacketWriter = new osu.Bancho.Writer;
let osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectatorJoined(user.id);
userToSpectate.addActionToQueue(osuPacketWriter.toBuffer);
osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.FellowSpectatorJoined(user.id);
@ -45,7 +45,7 @@ export class SpectatorManager {
return;
}
const osuPacketWriter = new osu.Bancho.Writer;
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectateFrames(spectateFrameData);
user.spectatorStream.Send(osuPacketWriter.toBuffer);
@ -58,7 +58,7 @@ export class SpectatorManager {
const spectatedUser = user.spectatingUser;
let osuPacketWriter = new osu.Bancho.Writer;
let osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectatorLeft(user.id);
spectatedUser.addActionToQueue(osuPacketWriter.toBuffer);
@ -69,7 +69,7 @@ export class SpectatorManager {
user.spectatingUser = undefined;
if (stream.IsActive) {
osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.FellowSpectatorLeft(user.id);
stream.Send(osuPacketWriter.toBuffer);
} 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";
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 { User } from "./User";
const osu = require("osu-packet");
export class Channel {
public name:string;
public description:string;
public stream:DataStream;
private isLocked:boolean = false;
public isLocked: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.description = description;
this.stream = stream;
this._isSpecial = isSpecial;
const bot = sharedContent.users.getByKey("bot");
if (!(bot instanceof User)) {
throw "Something has gone horribly wrong, the bot user doesn't exist!";
}
this.botUser = bot;
this.bot = shared.bot;
}
public get self() {
return this;
}
public get isSpecial() {
public get isSpecial() : boolean {
return this._isSpecial;
}
public get userCount() {
public get userCount() : number {
return this.stream.userCount;
}
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);
}
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) {
const osuPacketWriter = new osu.Bancho.Writer;
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({
sendingClient: this.botUser.username,
sendingClient: this.bot.user.username,
message: message,
target: this.name,
senderId: this.botUser.id
senderId: this.bot.user.id
});
if (sendTo instanceof User) {
@ -82,7 +66,7 @@ export class Channel {
}
public SendSystemMessage(message:string, sendTo?:User) {
const osuPacketWriter = new osu.Bancho.Writer;
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({
sendingClient: "System",
message: message,
@ -99,12 +83,15 @@ export class Channel {
public Join(user:User) {
this.stream.AddUser(user);
const osuPacketWriter = new osu.Bancho.Writer;
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.ChannelJoinSuccess(this.name);
user.addActionToQueue(osuPacketWriter.toBuffer);
}
public Leave(user: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 { hexlify } from "../Util";
type DeleteFunction = (dataStream:DataStream) => void;
export class DataStream {
private users:UserArray = new UserArray();
public readonly name:string;
private readonly parent:DataStreamArray;
private readonly removeWhenEmpty:boolean;
private inactive:boolean = false;
public onDelete?:DeleteFunction;
public constructor(name:string, parent:DataStreamArray, removeWhenEmpty:boolean) {
this.name = name;
@ -32,6 +35,10 @@ export class DataStream {
return this.users.getLength();
}
public HasUser(user:User) : boolean {
return this.users.getByKey(user.uuid) !== undefined;
}
public AddUser(user:User) : void {
this.checkInactive();
@ -54,6 +61,9 @@ export class DataStream {
}
public Delete() {
if (typeof(this.onDelete) === "function") {
this.onDelete(this);
}
this.parent.DeleteStream(this);
}

View file

@ -7,7 +7,7 @@ export class Database {
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({
connectionLimit: Database.CONNECTION_LIMIT,
host: databaseAddress,
@ -17,27 +17,7 @@ export class Database {
database: databaseName
});
const classCreationTime:number = Date.now();
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);
ConsoleHelper.printInfo(`Connected DB connection pool. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`);
}
public query(query = "", data?:Array<any>) {

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { Match } from "./Match";
export class MatchArray extends FunkyArray<Match> {
public getById(id:number) : Match | undefined {
for (let match of this.getIterableItems()) {
if (match.matchId == id) {
if (match.matchId === id) {
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 { DataStream } from "./DataStream";
import { StatusUpdate } from "../packets/StatusUpdate";
import { SharedContent } from "../interfaces/SharedContent";
import { Shared } from "../objects/Shared";
import { Slot } from "./Slot";
const rankingModes = [
@ -15,7 +15,7 @@ const rankingModes = [
export class User {
private static readonly EMPTY_BUFFER = Buffer.alloc(0);
public sharedContent:SharedContent;
public shared:Shared;
public id:number;
public username:string;
@ -65,12 +65,12 @@ export class User {
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.username = username;
this.uuid = uuid;
this.sharedContent = sharedContent;
this.shared = shared;
}
// Concats new actions to the user's queue
@ -83,7 +83,7 @@ export class User {
}
// Updates the user's current action
updatePresence(action:any) : void {
updatePresence(action:any) {
this.actionID = action.status;
this.actionText = action.statusText;
this.beatmapChecksum = action.beatmapChecksum;
@ -97,10 +97,10 @@ export class User {
}
// Gets the user's score information from the database and caches it
async updateUserInfo(forceUpdate:boolean = false) : Promise<void> {
const userScoreDB = await this.sharedContent.database.query("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]);
async updateUserInfo(forceUpdate:boolean = false) {
const userScoreDB = await this.shared.database.query("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]);
const 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";

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);
if (user.spectatorStream != null) {
const statusUpdate = StatusUpdate(user.sharedContent, user.id);
user.spectatorStream.Send(statusUpdate);
const statusUpdate = StatusUpdate(user.shared, user.id);
if (statusUpdate instanceof Buffer) {
user.spectatorStream.Send(statusUpdate);
}
}
}

View file

@ -8,12 +8,12 @@ export async function Logout(user:User) {
const logoutStartTime = Date.now();
user.sharedContent.streams.RemoveUserFromAllStreams(user);
user.shared.streams.RemoveUserFromAllStreams(user);
// 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}]`);
}

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 { 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
// Create new osu packet writer
const osuPacketWriter = new osu.Bancho.Writer;
let sharedContent:SharedContent;
const osuPacketWriter = osu.Bancho.Writer();
let shared:Shared;
if (arg0 instanceof User) {
sharedContent = arg0.sharedContent;
shared = arg0.shared;
} else {
sharedContent = arg0;
shared = arg0;
}
// Get user's class
const userData = sharedContent.users.getById(id);
const userData = shared.users.getById(id);
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";
const osu = require("osu-packet");
export function UserPresence(arg0:User | SharedContent, id:number) {
const osuPacketWriter = new osu.Bancho.Writer;
let sharedContent:SharedContent;
export function UserPresence(arg0:User | Shared, id:number) {
const osuPacketWriter = osu.Bancho.Writer();
let shared:Shared;
if (arg0 instanceof User) {
sharedContent = arg0.sharedContent;
shared = arg0.shared;
} else {
sharedContent = arg0;
shared = arg0;
}
const userData = sharedContent.users.getById(id);
const userData = shared.users.getById(id);
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";
const osu = require("osu-packet");
export function UserPresenceBundle(arg0:User | SharedContent) : Buffer {
const osuPacketWriter = new osu.Bancho.Writer;
let sharedContent:SharedContent;
export function UserPresenceBundle(arg0:User | Shared) : Buffer {
const osuPacketWriter = osu.Bancho.Writer();
let shared:Shared;
if (arg0 instanceof User) {
sharedContent = arg0.sharedContent;
shared = arg0.shared;
} else {
sharedContent = arg0;
shared = arg0;
}
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);
}

View file

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