diff --git a/server/BanchoServer.ts b/server/BanchoServer.ts index 427ffd3..c6bf631 100644 --- a/server/BanchoServer.ts +++ b/server/BanchoServer.ts @@ -1,4 +1,5 @@ import { ConsoleHelper } from "../ConsoleHelper"; +import { Channel } from "./objects/Channel"; import { ChatManager } from "./ChatManager"; import { Database } from "./objects/Database"; import { LatLng } from "./objects/LatLng"; @@ -16,8 +17,6 @@ const config:any = JSON.parse(readFileSync("./config.json").toString()); // TODO: Port osu-packet to TypeScript const osu = require("osu-packet"); -/*Streams = require("./Streams.js");*/ - const DB:Database = new Database(config.database.address, config.database.port, config.database.username, config.database.password, config.database.name, async () => { // Close any unclosed db matches on startup DB.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL"); @@ -32,7 +31,7 @@ const streams:DataStreamArray = new DataStreamArray(); // ChatManager const chatManager:ChatManager = new ChatManager(streams); -chatManager.AddChatChannel("osu", "The main channel"); +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"); @@ -82,42 +81,25 @@ if (config.redis.enabled) { })(); } else ConsoleHelper.printWarn("Redis is disabled!"); +// Import packets +import { ChangeAction } from "./packets/ChangeAction"; +import { Logout } from "./packets/Logout"; +import { UserPresence } from "./packets/UserPresence"; +import { UserStatsRequest } from "./packets/UserStatsRequest"; +import { UserPresenceBundle } from "./packets/UserPresenceBundle"; + // User timeout interval setInterval(() => { for (let User of users.getIterableItems()) { if (User.uuid == "bot") continue; // Ignore the bot // Logout this user, they're clearly gone. - // if (Date.now() >= User.timeoutTime) - // Logout(User); + if (Date.now() >= User.timeoutTime) { + Logout(User); + } } }, 10000); -// Include packets -/*const ChangeAction = require("./Packets/ChangeAction.js"), - SendPublicMessage = require("./Packets/SendPublicMessage.js"), - Logout = require("./Packets/Logout.js"), - Spectator = require("./Spectator.js"), - SendPrivateMessage = require("./Packets/SendPrivateMessage.js"), - MultiplayerManager = require("./MultiplayerManager.js"), - SetAwayMessage = require("./Packets/SetAwayMessage.js"), - ChannelJoin = require("./Packets/ChannelJoin.js"), - ChannelPart = require("./Packets/ChannelPart.js"), - AddFriend = require("./Packets/AddFriend.js"), - RemoveFriend = require("./Packets/RemoveFriend.js"), - UserPresenceBundle = require("./Packets/UserPresenceBundle.js"), - UserPresence = require("./Packets/UserPresence.js"), - UserStatsRequest = require("./Packets/UserStatsRequest.js"), - MultiplayerInvite = require("./Packets/MultiplayerInvite.js"), - TourneyMatchSpecialInfo = require("./Packets/TourneyMatchSpecialInfo.js"), - TourneyMatchJoinChannel = require("./Packets/TourneyMatchSpecialInfo.js"), - TourneyMatchLeaveChannel = require("./Packets/TourneyLeaveMatchChannel.js");*/ -import { ChangeAction } from "./packets/ChangeAction"; -import { Logout } from "./packets/Logout"; -import { UserPresence } from "./packets/UserPresence"; -import { UserStatsRequest } from "./packets/UserStatsRequest"; -import { UserPresenceBundle } from "./packets/UserPresenceBundle"; - const EMPTY_BUFFER = Buffer.alloc(0); export async function HandleRequest(req:Request, res:Response, packet:Buffer) { @@ -163,7 +145,10 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) { break; case Packets.Client_SendPublicMessage: - //SendPublicMessage(PacketUser, CurrentPacket.data); + let channel = chatManager.GetChannelByName(CurrentPacket.data.target); + if (channel instanceof Channel) { + channel.SendMessage(PacketUser, CurrentPacket.data.message); + } break; case Packets.Client_Logout: diff --git a/server/ChatManager.ts b/server/ChatManager.ts index ac01449..0da71b7 100644 --- a/server/ChatManager.ts +++ b/server/ChatManager.ts @@ -2,9 +2,12 @@ import { Channel } from "./objects/Channel"; import { ConsoleHelper } from "../ConsoleHelper"; import { FunkyArray } from "./objects/FunkyArray"; import { DataStreamArray } from "./objects/DataStreamArray"; +import { User } from "./objects/User"; +const osu = require("osu-packet"); export class ChatManager { public chatChannels:FunkyArray = new FunkyArray(); + public forceJoinChannels:FunkyArray = new FunkyArray(); public streams:DataStreamArray; public constructor(streams:DataStreamArray) { @@ -13,7 +16,48 @@ export class ChatManager { public AddChatChannel(name:string, description:string, forceJoin:boolean = false) { const stream = this.streams.CreateStream(`chat_channel:${name}`, false); - this.chatChannels.add(name, new Channel(name, description, stream, forceJoin)); + const channel = new Channel(`#${name}`, description, stream); + this.chatChannels.add(channel.name, channel); + if (forceJoin) { + this.forceJoinChannels.add(name, channel); + } ConsoleHelper.printChat(`Created chat channel [${name}]`); } + + public RemoveChatChannel(channel:Channel | string) { + if (channel instanceof Channel) { + channel.stream.Delete(); + this.chatChannels.remove(channel.stream.name); + this.forceJoinChannels.remove(channel.stream.name) + } else { + const chatChannel = this.GetChannelByName(channel); + if (chatChannel instanceof Channel) { + chatChannel.stream.Delete(); + this.chatChannels.remove(chatChannel.stream.name); + this.forceJoinChannels.remove(chatChannel.stream.name) + } + } + } + + public GetChannelByName(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; + for (let channel of this.chatChannels.getIterableItems()) { + osuPacketWriter.ChannelAvailable({ + channelName: channel.name, + channelTopic: channel.description, + channelUserCount: channel.userCount + }); + } + user.addActionToQueue(osuPacketWriter.toBuffer); + } } \ No newline at end of file diff --git a/server/LoginProcess.ts b/server/LoginProcess.ts index b5ab519..e812141 100644 --- a/server/LoginProcess.ts +++ b/server/LoginProcess.ts @@ -192,23 +192,13 @@ export async function LoginProcess(req:Request, res:Response, packet:Buffer, dat // peppy pls, why osuPacketWriter.ChannelListingComplete(); - // Add user to #osu - osuPacketWriter.ChannelJoinSuccess("#osu"); - //if (!Streams.isUserInStream("#osu", newUser.uuid)) - // Streams.addUserToStream("#osu", newUser.uuid); - - // List all channels out to the client - /*for (let i = 0; i < global.channels.length; i++) { - osuPacketWriter.ChannelAvailable({ - channelName: global.channels[i].channelName, - channelTopic: global.channels[i].channelTopic, - channelUserCount: global.channels[i].channelUserCount - }); - }*/ + // Setup chat + chatManager.ForceJoinChannels(newUser); + chatManager.SendChannelListing(newUser); // Construct user's friends list const userFriends = await database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]); - let friendsArray = new Array; + const friendsArray:Array = new Array(); for (let i = 0; i < userFriends.length; i++) { friendsArray.push(userFriends[i].friendsWith); } diff --git a/server/Util.ts b/server/Util.ts index 2c30aff..c4995f0 100644 --- a/server/Util.ts +++ b/server/Util.ts @@ -23,4 +23,18 @@ export function generateSession() : Promise { export function generateSessionSync() : string { return randomBytes(12).toString("hex"); +} + +export function hexlify(data:Buffer) : string { + let out:string = ""; + for (let i = 0; i < data.length; i++) { + const hex = data[i].toString(16); + if (hex.length === 1) { + out += `0${hex.toUpperCase()},`; + } else { + out += `${hex.toUpperCase()},`; + } + } + + return out.slice(0, out.length - 1); } \ No newline at end of file diff --git a/server/objects/Channel.ts b/server/objects/Channel.ts index 4bffea4..d6c7895 100644 --- a/server/objects/Channel.ts +++ b/server/objects/Channel.ts @@ -1,20 +1,70 @@ import { DataStream } from "./DataStream"; +import { User } from "./User"; +const osu = require("osu-packet"); export class Channel { public name:string; public description:string; - public userCount:number = 0; - private stream:DataStream; - private forceJoin:boolean; + public stream:DataStream; + private isLocked:boolean = false; - public constructor(name:string, description:string, stream:DataStream, forceJoin:boolean = false) { + public constructor(name:string, description:string, stream:DataStream) { this.name = name; this.description = description; this.stream = stream; - this.forceJoin = forceJoin; } - public SendMessage(message:string) { + public get userCount() { + return this.stream.userCount; + } + public SendMessage(sender:User, message:string) { + const isBotCommand = message[0] === "!"; + + if (this.isLocked && !isBotCommand) { + return this.SendSystemMessage("This channel is currently locked", sender); + } + + if (isBotCommand) { + if (message.split(" ")[0] === "!lock") { + this.isLocked = true; + } + } + + 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 SendSystemMessage(message:string, sendTo?:User) { + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.SendMessage({ + sendingClient: "System", + message: message, + target: this.name, + senderId: 1 + }); + + if (sendTo instanceof User) { + sendTo.addActionToQueue(osuPacketWriter.toBuffer); + } else { + this.stream.Send(osuPacketWriter.toBuffer); + } + } + + public Join(user:User) { + this.stream.AddUser(user); + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.ChannelJoinSuccess(this.name); + user.addActionToQueue(osuPacketWriter.toBuffer); + } + + public Leave(user:User) { + this.stream.RemoveUser(user); } } \ No newline at end of file diff --git a/server/objects/DataStream.ts b/server/objects/DataStream.ts index 04faeff..29fd5a0 100644 --- a/server/objects/DataStream.ts +++ b/server/objects/DataStream.ts @@ -3,12 +3,14 @@ import { Constants } from "../../Constants"; import { DataStreamArray } from "./DataStreamArray"; import { User } from "./User"; import { UserArray } from "./UserArray"; +import { hexlify } from "../Util"; export class DataStream { private users:UserArray = new UserArray(); - private readonly name:string; + public readonly name:string; private readonly parent:DataStreamArray; private readonly removeWhenEmpty:boolean; + private inactive:boolean = false; public constructor(name:string, parent:DataStreamArray, removeWhenEmpty:boolean) { this.name = name; @@ -16,30 +18,66 @@ export class DataStream { this.removeWhenEmpty = removeWhenEmpty; } + private checkInactive() { + if (this.inactive) { + throw `Stream ${this.name} is inactive (deleted) and cannot be used here.`; + } + } + + public get userCount() : number { + return this.users.getLength(); + } + public AddUser(user:User) : void { + this.checkInactive(); + if (!(user.uuid in this.users.getItems())) { this.users.add(user.uuid, user); - ConsoleHelper.printStream(`Added user [${user.username}|${user.uuid}] to stream [${this.name}]`); + ConsoleHelper.printStream(`Added [${user.username}] to stream [${this.name}]`); } } public RemoveUser(user:User) : void { + this.checkInactive(); + if (user.uuid in this.users.getItems()) { this.users.remove(user.uuid); - ConsoleHelper.printStream(`Removed user [${user.username}|${user.uuid}] from stream [${this.name}]`); + ConsoleHelper.printStream(`Removed [${user.username}] from stream [${this.name}]`); } if (this.removeWhenEmpty && this.users.getLength() === 0) { - this.parent.remove(this.name); + this.Delete(); } } + public Delete() { + this.parent.DeleteStream(this); + } + + public Deactivate() { + this.inactive = true; + } + public Send(data:Buffer) { + this.checkInactive(); + for (let user of this.users.getIterableItems()) { user.addActionToQueue(data); } - if (Constants.DEBUG) { ConsoleHelper.printStream(`Sent [${data.toString()}] to all users in stream [${this.name}]`); } } + + public SendWithExclusion(data:Buffer, exclude:User) { + this.checkInactive(); + + for (let user of this.users.getIterableItems()) { + if (user.uuid !== exclude.uuid) { + user.addActionToQueue(data); + } + } + if (Constants.DEBUG) { + ConsoleHelper.printStream(`Sent Buffer<${hexlify(data)}> to all users in stream [${this.name}] excluding user [${exclude.username}]`); + } + } } \ No newline at end of file diff --git a/server/objects/DataStreamArray.ts b/server/objects/DataStreamArray.ts index ca6aeba..2491060 100644 --- a/server/objects/DataStreamArray.ts +++ b/server/objects/DataStreamArray.ts @@ -10,6 +10,21 @@ export class DataStreamArray extends FunkyArray { return dataStream; } + public DeleteStream(stream:DataStream | string) { + const isObject = stream instanceof DataStream; + if (isObject) { + stream.Deactivate(); + this.remove(stream.name); + } else { + const dso = this.getByKey(stream); + if (dso != null) { + dso.Deactivate(); + } + this.remove(stream); + } + ConsoleHelper.printStream(`Deleted stream [${isObject ? stream.name : stream}]`); + } + public RemoveUserFromAllStreams(user:User) { for (let stream of this.getIterableItems()) { stream.RemoveUser(user); diff --git a/server/objects/User.ts b/server/objects/User.ts index 38663b8..801a7c2 100644 --- a/server/objects/User.ts +++ b/server/objects/User.ts @@ -6,6 +6,7 @@ import { DataStream } from "./DataStream"; import { UserArray } from "./UserArray"; import { DataStreamArray } from "./DataStreamArray"; import { ChatManager } from "../ChatManager"; +import { StatusUpdate } from "../packets/StatusUpdate"; //const StatusUpdate = require("./Packets/StatusUpdate.js"); const rankingModes = [ @@ -146,7 +147,7 @@ export class User { else this.pp = 0; if (userScoreUpdate || forceUpdate) { - //StatusUpdate(this, this.id); + StatusUpdate(this, this.id); } } } \ No newline at end of file