commit 47305d612a568a6800419daa18dafe6763dab063 Author: Nyo Date: Tue Apr 19 19:40:59 2016 +0200 Moved pep.py to another repo diff --git a/__pycache__/actions.cpython-35.pyc b/__pycache__/actions.cpython-35.pyc new file mode 100644 index 0000000..afe0fc4 Binary files /dev/null and b/__pycache__/actions.cpython-35.pyc differ diff --git a/__pycache__/banchoConfig.cpython-35.pyc b/__pycache__/banchoConfig.cpython-35.pyc new file mode 100644 index 0000000..be916b2 Binary files /dev/null and b/__pycache__/banchoConfig.cpython-35.pyc differ diff --git a/__pycache__/bcolors.cpython-35.pyc b/__pycache__/bcolors.cpython-35.pyc new file mode 100644 index 0000000..a972d29 Binary files /dev/null and b/__pycache__/bcolors.cpython-35.pyc differ diff --git a/__pycache__/cantSpectateEvent.cpython-35.pyc b/__pycache__/cantSpectateEvent.cpython-35.pyc new file mode 100644 index 0000000..c059156 Binary files /dev/null and b/__pycache__/cantSpectateEvent.cpython-35.pyc differ diff --git a/__pycache__/changeActionEvent.cpython-35.pyc b/__pycache__/changeActionEvent.cpython-35.pyc new file mode 100644 index 0000000..1b08a01 Binary files /dev/null and b/__pycache__/changeActionEvent.cpython-35.pyc differ diff --git a/__pycache__/changeMatchModsEvent.cpython-35.pyc b/__pycache__/changeMatchModsEvent.cpython-35.pyc new file mode 100644 index 0000000..84e47a9 Binary files /dev/null and b/__pycache__/changeMatchModsEvent.cpython-35.pyc differ diff --git a/__pycache__/changeMatchPasswordEvent.cpython-35.pyc b/__pycache__/changeMatchPasswordEvent.cpython-35.pyc new file mode 100644 index 0000000..bf73206 Binary files /dev/null and b/__pycache__/changeMatchPasswordEvent.cpython-35.pyc differ diff --git a/__pycache__/changeMatchSettingsEvent.cpython-35.pyc b/__pycache__/changeMatchSettingsEvent.cpython-35.pyc new file mode 100644 index 0000000..14b9485 Binary files /dev/null and b/__pycache__/changeMatchSettingsEvent.cpython-35.pyc differ diff --git a/__pycache__/changeSlotEvent.cpython-35.pyc b/__pycache__/changeSlotEvent.cpython-35.pyc new file mode 100644 index 0000000..556e110 Binary files /dev/null and b/__pycache__/changeSlotEvent.cpython-35.pyc differ diff --git a/__pycache__/channel.cpython-35.pyc b/__pycache__/channel.cpython-35.pyc new file mode 100644 index 0000000..eae749e Binary files /dev/null and b/__pycache__/channel.cpython-35.pyc differ diff --git a/__pycache__/channelJoinEvent.cpython-35.pyc b/__pycache__/channelJoinEvent.cpython-35.pyc new file mode 100644 index 0000000..582e289 Binary files /dev/null and b/__pycache__/channelJoinEvent.cpython-35.pyc differ diff --git a/__pycache__/channelList.cpython-35.pyc b/__pycache__/channelList.cpython-35.pyc new file mode 100644 index 0000000..96ad7b9 Binary files /dev/null and b/__pycache__/channelList.cpython-35.pyc differ diff --git a/__pycache__/channelPartEvent.cpython-35.pyc b/__pycache__/channelPartEvent.cpython-35.pyc new file mode 100644 index 0000000..f959b06 Binary files /dev/null and b/__pycache__/channelPartEvent.cpython-35.pyc differ diff --git a/__pycache__/clientPackets.cpython-35.pyc b/__pycache__/clientPackets.cpython-35.pyc new file mode 100644 index 0000000..8d03235 Binary files /dev/null and b/__pycache__/clientPackets.cpython-35.pyc differ diff --git a/__pycache__/config.cpython-35.pyc b/__pycache__/config.cpython-35.pyc new file mode 100644 index 0000000..9d8a639 Binary files /dev/null and b/__pycache__/config.cpython-35.pyc differ diff --git a/__pycache__/consoleHelper.cpython-35.pyc b/__pycache__/consoleHelper.cpython-35.pyc new file mode 100644 index 0000000..765edaf Binary files /dev/null and b/__pycache__/consoleHelper.cpython-35.pyc differ diff --git a/__pycache__/countryHelper.cpython-35.pyc b/__pycache__/countryHelper.cpython-35.pyc new file mode 100644 index 0000000..c25eb06 Binary files /dev/null and b/__pycache__/countryHelper.cpython-35.pyc differ diff --git a/__pycache__/createMatchEvent.cpython-35.pyc b/__pycache__/createMatchEvent.cpython-35.pyc new file mode 100644 index 0000000..c3bf29d Binary files /dev/null and b/__pycache__/createMatchEvent.cpython-35.pyc differ diff --git a/__pycache__/crypt.cpython-35.pyc b/__pycache__/crypt.cpython-35.pyc new file mode 100644 index 0000000..6699073 Binary files /dev/null and b/__pycache__/crypt.cpython-35.pyc differ diff --git a/__pycache__/dataTypes.cpython-35.pyc b/__pycache__/dataTypes.cpython-35.pyc new file mode 100644 index 0000000..8ba1ffe Binary files /dev/null and b/__pycache__/dataTypes.cpython-35.pyc differ diff --git a/__pycache__/databaseHelper.cpython-35.pyc b/__pycache__/databaseHelper.cpython-35.pyc new file mode 100644 index 0000000..a8a691b Binary files /dev/null and b/__pycache__/databaseHelper.cpython-35.pyc differ diff --git a/__pycache__/exceptions.cpython-35.pyc b/__pycache__/exceptions.cpython-35.pyc new file mode 100644 index 0000000..42b6555 Binary files /dev/null and b/__pycache__/exceptions.cpython-35.pyc differ diff --git a/__pycache__/fokabot.cpython-35.pyc b/__pycache__/fokabot.cpython-35.pyc new file mode 100644 index 0000000..24744a8 Binary files /dev/null and b/__pycache__/fokabot.cpython-35.pyc differ diff --git a/__pycache__/fokabotCommands.cpython-35.pyc b/__pycache__/fokabotCommands.cpython-35.pyc new file mode 100644 index 0000000..4ec4715 Binary files /dev/null and b/__pycache__/fokabotCommands.cpython-35.pyc differ diff --git a/__pycache__/friendAddEvent.cpython-35.pyc b/__pycache__/friendAddEvent.cpython-35.pyc new file mode 100644 index 0000000..7ce20e1 Binary files /dev/null and b/__pycache__/friendAddEvent.cpython-35.pyc differ diff --git a/__pycache__/friendRemoveEvent.cpython-35.pyc b/__pycache__/friendRemoveEvent.cpython-35.pyc new file mode 100644 index 0000000..a40570f Binary files /dev/null and b/__pycache__/friendRemoveEvent.cpython-35.pyc differ diff --git a/__pycache__/gameModes.cpython-35.pyc b/__pycache__/gameModes.cpython-35.pyc new file mode 100644 index 0000000..4c9db92 Binary files /dev/null and b/__pycache__/gameModes.cpython-35.pyc differ diff --git a/__pycache__/generalFunctions.cpython-35.pyc b/__pycache__/generalFunctions.cpython-35.pyc new file mode 100644 index 0000000..1f7868f Binary files /dev/null and b/__pycache__/generalFunctions.cpython-35.pyc differ diff --git a/__pycache__/glob.cpython-35.pyc b/__pycache__/glob.cpython-35.pyc new file mode 100644 index 0000000..110f109 Binary files /dev/null and b/__pycache__/glob.cpython-35.pyc differ diff --git a/__pycache__/joinLobbyEvent.cpython-35.pyc b/__pycache__/joinLobbyEvent.cpython-35.pyc new file mode 100644 index 0000000..f997a9c Binary files /dev/null and b/__pycache__/joinLobbyEvent.cpython-35.pyc differ diff --git a/__pycache__/joinMatchEvent.cpython-35.pyc b/__pycache__/joinMatchEvent.cpython-35.pyc new file mode 100644 index 0000000..b98804b Binary files /dev/null and b/__pycache__/joinMatchEvent.cpython-35.pyc differ diff --git a/__pycache__/locationHelper.cpython-35.pyc b/__pycache__/locationHelper.cpython-35.pyc new file mode 100644 index 0000000..f3fc500 Binary files /dev/null and b/__pycache__/locationHelper.cpython-35.pyc differ diff --git a/__pycache__/loginEvent.cpython-35.pyc b/__pycache__/loginEvent.cpython-35.pyc new file mode 100644 index 0000000..b0301d2 Binary files /dev/null and b/__pycache__/loginEvent.cpython-35.pyc differ diff --git a/__pycache__/logoutEvent.cpython-35.pyc b/__pycache__/logoutEvent.cpython-35.pyc new file mode 100644 index 0000000..a9cc3a5 Binary files /dev/null and b/__pycache__/logoutEvent.cpython-35.pyc differ diff --git a/__pycache__/match.cpython-35.pyc b/__pycache__/match.cpython-35.pyc new file mode 100644 index 0000000..8bd386f Binary files /dev/null and b/__pycache__/match.cpython-35.pyc differ diff --git a/__pycache__/matchBeatmapEvent.cpython-35.pyc b/__pycache__/matchBeatmapEvent.cpython-35.pyc new file mode 100644 index 0000000..4bf024b Binary files /dev/null and b/__pycache__/matchBeatmapEvent.cpython-35.pyc differ diff --git a/__pycache__/matchChangeTeamEvent.cpython-35.pyc b/__pycache__/matchChangeTeamEvent.cpython-35.pyc new file mode 100644 index 0000000..0d0f185 Binary files /dev/null and b/__pycache__/matchChangeTeamEvent.cpython-35.pyc differ diff --git a/__pycache__/matchCompleteEvent.cpython-35.pyc b/__pycache__/matchCompleteEvent.cpython-35.pyc new file mode 100644 index 0000000..53bacd5 Binary files /dev/null and b/__pycache__/matchCompleteEvent.cpython-35.pyc differ diff --git a/__pycache__/matchFailedEvent.cpython-35.pyc b/__pycache__/matchFailedEvent.cpython-35.pyc new file mode 100644 index 0000000..803218d Binary files /dev/null and b/__pycache__/matchFailedEvent.cpython-35.pyc differ diff --git a/__pycache__/matchFramesEvent.cpython-35.pyc b/__pycache__/matchFramesEvent.cpython-35.pyc new file mode 100644 index 0000000..6fae840 Binary files /dev/null and b/__pycache__/matchFramesEvent.cpython-35.pyc differ diff --git a/__pycache__/matchHasBeatmapEvent.cpython-35.pyc b/__pycache__/matchHasBeatmapEvent.cpython-35.pyc new file mode 100644 index 0000000..d38e988 Binary files /dev/null and b/__pycache__/matchHasBeatmapEvent.cpython-35.pyc differ diff --git a/__pycache__/matchInviteEvent.cpython-35.pyc b/__pycache__/matchInviteEvent.cpython-35.pyc new file mode 100644 index 0000000..e247b60 Binary files /dev/null and b/__pycache__/matchInviteEvent.cpython-35.pyc differ diff --git a/__pycache__/matchList.cpython-35.pyc b/__pycache__/matchList.cpython-35.pyc new file mode 100644 index 0000000..fdcc11e Binary files /dev/null and b/__pycache__/matchList.cpython-35.pyc differ diff --git a/__pycache__/matchLockEvent.cpython-35.pyc b/__pycache__/matchLockEvent.cpython-35.pyc new file mode 100644 index 0000000..0266a48 Binary files /dev/null and b/__pycache__/matchLockEvent.cpython-35.pyc differ diff --git a/__pycache__/matchModModes.cpython-35.pyc b/__pycache__/matchModModes.cpython-35.pyc new file mode 100644 index 0000000..3043031 Binary files /dev/null and b/__pycache__/matchModModes.cpython-35.pyc differ diff --git a/__pycache__/matchNoBeatmapEvent.cpython-35.pyc b/__pycache__/matchNoBeatmapEvent.cpython-35.pyc new file mode 100644 index 0000000..720fafb Binary files /dev/null and b/__pycache__/matchNoBeatmapEvent.cpython-35.pyc differ diff --git a/__pycache__/matchPlayerLoadEvent.cpython-35.pyc b/__pycache__/matchPlayerLoadEvent.cpython-35.pyc new file mode 100644 index 0000000..ca7ca49 Binary files /dev/null and b/__pycache__/matchPlayerLoadEvent.cpython-35.pyc differ diff --git a/__pycache__/matchReadyEvent.cpython-35.pyc b/__pycache__/matchReadyEvent.cpython-35.pyc new file mode 100644 index 0000000..ca7ce38 Binary files /dev/null and b/__pycache__/matchReadyEvent.cpython-35.pyc differ diff --git a/__pycache__/matchScoringTypes.cpython-35.pyc b/__pycache__/matchScoringTypes.cpython-35.pyc new file mode 100644 index 0000000..6dcdcae Binary files /dev/null and b/__pycache__/matchScoringTypes.cpython-35.pyc differ diff --git a/__pycache__/matchSkipEvent.cpython-35.pyc b/__pycache__/matchSkipEvent.cpython-35.pyc new file mode 100644 index 0000000..e7cf9b8 Binary files /dev/null and b/__pycache__/matchSkipEvent.cpython-35.pyc differ diff --git a/__pycache__/matchStartEvent.cpython-35.pyc b/__pycache__/matchStartEvent.cpython-35.pyc new file mode 100644 index 0000000..486fcd5 Binary files /dev/null and b/__pycache__/matchStartEvent.cpython-35.pyc differ diff --git a/__pycache__/matchTeamTypes.cpython-35.pyc b/__pycache__/matchTeamTypes.cpython-35.pyc new file mode 100644 index 0000000..4b029bf Binary files /dev/null and b/__pycache__/matchTeamTypes.cpython-35.pyc differ diff --git a/__pycache__/matchTeams.cpython-35.pyc b/__pycache__/matchTeams.cpython-35.pyc new file mode 100644 index 0000000..2eb2cc1 Binary files /dev/null and b/__pycache__/matchTeams.cpython-35.pyc differ diff --git a/__pycache__/matchTransferHostEvent.cpython-35.pyc b/__pycache__/matchTransferHostEvent.cpython-35.pyc new file mode 100644 index 0000000..bf9d90c Binary files /dev/null and b/__pycache__/matchTransferHostEvent.cpython-35.pyc differ diff --git a/__pycache__/mods.cpython-35.pyc b/__pycache__/mods.cpython-35.pyc new file mode 100644 index 0000000..d9a7361 Binary files /dev/null and b/__pycache__/mods.cpython-35.pyc differ diff --git a/__pycache__/osuToken.cpython-35.pyc b/__pycache__/osuToken.cpython-35.pyc new file mode 100644 index 0000000..79035e7 Binary files /dev/null and b/__pycache__/osuToken.cpython-35.pyc differ diff --git a/__pycache__/packetHelper.cpython-35.pyc b/__pycache__/packetHelper.cpython-35.pyc new file mode 100644 index 0000000..2565b23 Binary files /dev/null and b/__pycache__/packetHelper.cpython-35.pyc differ diff --git a/__pycache__/packetIDs.cpython-35.pyc b/__pycache__/packetIDs.cpython-35.pyc new file mode 100644 index 0000000..7f8406c Binary files /dev/null and b/__pycache__/packetIDs.cpython-35.pyc differ diff --git a/__pycache__/partLobbyEvent.cpython-35.pyc b/__pycache__/partLobbyEvent.cpython-35.pyc new file mode 100644 index 0000000..db3fd6e Binary files /dev/null and b/__pycache__/partLobbyEvent.cpython-35.pyc differ diff --git a/__pycache__/partMatchEvent.cpython-35.pyc b/__pycache__/partMatchEvent.cpython-35.pyc new file mode 100644 index 0000000..d343976 Binary files /dev/null and b/__pycache__/partMatchEvent.cpython-35.pyc differ diff --git a/__pycache__/passwordHelper.cpython-35.pyc b/__pycache__/passwordHelper.cpython-35.pyc new file mode 100644 index 0000000..ca6cb16 Binary files /dev/null and b/__pycache__/passwordHelper.cpython-35.pyc differ diff --git a/__pycache__/responseHelper.cpython-35.pyc b/__pycache__/responseHelper.cpython-35.pyc new file mode 100644 index 0000000..dd3077f Binary files /dev/null and b/__pycache__/responseHelper.cpython-35.pyc differ diff --git a/__pycache__/sendPrivateMessageEvent.cpython-35.pyc b/__pycache__/sendPrivateMessageEvent.cpython-35.pyc new file mode 100644 index 0000000..4105619 Binary files /dev/null and b/__pycache__/sendPrivateMessageEvent.cpython-35.pyc differ diff --git a/__pycache__/sendPublicMessageEvent.cpython-35.pyc b/__pycache__/sendPublicMessageEvent.cpython-35.pyc new file mode 100644 index 0000000..456928a Binary files /dev/null and b/__pycache__/sendPublicMessageEvent.cpython-35.pyc differ diff --git a/__pycache__/serverPackets.cpython-35.pyc b/__pycache__/serverPackets.cpython-35.pyc new file mode 100644 index 0000000..af64272 Binary files /dev/null and b/__pycache__/serverPackets.cpython-35.pyc differ diff --git a/__pycache__/setAwayMessageEvent.cpython-35.pyc b/__pycache__/setAwayMessageEvent.cpython-35.pyc new file mode 100644 index 0000000..09ca770 Binary files /dev/null and b/__pycache__/setAwayMessageEvent.cpython-35.pyc differ diff --git a/__pycache__/slotStatuses.cpython-35.pyc b/__pycache__/slotStatuses.cpython-35.pyc new file mode 100644 index 0000000..72f03b8 Binary files /dev/null and b/__pycache__/slotStatuses.cpython-35.pyc differ diff --git a/__pycache__/spectateFramesEvent.cpython-35.pyc b/__pycache__/spectateFramesEvent.cpython-35.pyc new file mode 100644 index 0000000..28d2669 Binary files /dev/null and b/__pycache__/spectateFramesEvent.cpython-35.pyc differ diff --git a/__pycache__/startSpectatingEvent.cpython-35.pyc b/__pycache__/startSpectatingEvent.cpython-35.pyc new file mode 100644 index 0000000..55fa84a Binary files /dev/null and b/__pycache__/startSpectatingEvent.cpython-35.pyc differ diff --git a/__pycache__/stopSpectatingEvent.cpython-35.pyc b/__pycache__/stopSpectatingEvent.cpython-35.pyc new file mode 100644 index 0000000..85d5dba Binary files /dev/null and b/__pycache__/stopSpectatingEvent.cpython-35.pyc differ diff --git a/__pycache__/systemHelper.cpython-35.pyc b/__pycache__/systemHelper.cpython-35.pyc new file mode 100644 index 0000000..20dd895 Binary files /dev/null and b/__pycache__/systemHelper.cpython-35.pyc differ diff --git a/__pycache__/tokenList.cpython-35.pyc b/__pycache__/tokenList.cpython-35.pyc new file mode 100644 index 0000000..9335cc9 Binary files /dev/null and b/__pycache__/tokenList.cpython-35.pyc differ diff --git a/__pycache__/userHelper.cpython-35.pyc b/__pycache__/userHelper.cpython-35.pyc new file mode 100644 index 0000000..c0e0d7e Binary files /dev/null and b/__pycache__/userHelper.cpython-35.pyc differ diff --git a/__pycache__/userRanks.cpython-35.pyc b/__pycache__/userRanks.cpython-35.pyc new file mode 100644 index 0000000..e08bcd4 Binary files /dev/null and b/__pycache__/userRanks.cpython-35.pyc differ diff --git a/actions.py b/actions.py new file mode 100644 index 0000000..3811d4b --- /dev/null +++ b/actions.py @@ -0,0 +1,17 @@ +"""Contains user actions""" +#TODO: Uppercase +idle = 0 +afk = 1 +playing = 2 +editing = 3 +modding = 4 +multiplayer = 5 +watching = 6 +unknown = 7 +testing = 8 +submitting = 9 +paused = 10 +lobby = 11 +multiplaying= 12 +osuDirect = 13 +none = 14 diff --git a/banchoConfig.py b/banchoConfig.py new file mode 100644 index 0000000..ecdf05d --- /dev/null +++ b/banchoConfig.py @@ -0,0 +1,42 @@ +import glob +import generalFunctions + +class banchoConfig: + """ + Class that loads settings from bancho_settings db table + """ + + config = {"banchoMaintenance": False, "freeDirect": True, "menuIcon": "", "loginNotification": ""} + + def __init__(self, __loadFromDB = True): + """ + Initialize a banchoConfig object (and load bancho_settings from db) + + [__loadFromDB -- if True, load values from db. If False, don't load values. Default: True] + """ + + if __loadFromDB: + try: + self.loadSettings() + except: + raise + + def loadSettings(self): + """ + (re)load bancho_settings from DB and set values in config array + """ + + self.config["banchoMaintenance"] = generalFunctions.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'bancho_maintenance'")["value_int"]) + self.config["freeDirect"] = generalFunctions.stringToBool(glob.db.fetch("SELECT value_int FROM bancho_settings WHERE name = 'free_direct'")["value_int"]) + self.config["menuIcon"] = glob.db.fetch("SELECT value_string FROM bancho_settings WHERE name = 'menu_icon'")["value_string"] + self.config["loginNotification"] = glob.db.fetch("SELECT value_string FROM bancho_settings WHERE name = 'login_notification'")["value_string"] + + def setMaintenance(self, __maintenance): + """ + Turn on/off bancho maintenance mode. Write new value to db too + + __maintenance -- if True, turn on maintenance mode. If false, turn it off + """ + + self.config["banchoMaintenance"] = __maintenance + glob.db.execute("UPDATE bancho_settings SET value_int = ? WHERE name = 'bancho_maintenance'", [int(__maintenance)]) diff --git a/bcolors.py b/bcolors.py new file mode 100644 index 0000000..b0514a3 --- /dev/null +++ b/bcolors.py @@ -0,0 +1,9 @@ +"""Console colors""" +PINK = '\033[95m' +BLUE = '\033[94m' +GREEN = '\033[92m' +YELLOW = '\033[93m' +RED = '\033[91m' +ENDC = '\033[0m' +BOLD = '\033[1m' +UNDERLINE = '\033[4m' diff --git a/cantSpectateEvent.py b/cantSpectateEvent.py new file mode 100644 index 0000000..698ddc2 --- /dev/null +++ b/cantSpectateEvent.py @@ -0,0 +1,21 @@ +import glob +import serverPackets +import consoleHelper +import bcolors +import exceptions + +def handle(userToken, packetData): + # get usertoken data + userID = userToken.userID + + try: + # We don't have the beatmap, we can't spectate + target = userToken.spectating + targetToken = glob.tokens.getTokenFromUserID(target) + + # Send the packet to host + targetToken.enqueue(serverPackets.noSongSpectator(userID)) + except exceptions.tokenNotFoundException: + # Stop spectating if token not found + consoleHelper.printColored("[!] Spectator can't spectate: token not found", bcolors.RED) + userToken.stopSpectating() diff --git a/changeActionEvent.py b/changeActionEvent.py new file mode 100644 index 0000000..0a51659 --- /dev/null +++ b/changeActionEvent.py @@ -0,0 +1,26 @@ +import glob +import clientPackets +import serverPackets +import actions + +def handle(userToken, packetData): + # Get usertoken data + userID = userToken.userID + username = userToken.username + + # Change action packet + packetData = clientPackets.userActionChange(packetData) + + # Update our action id, text and md5 + userToken.actionID = packetData["actionID"] + userToken.actionText = packetData["actionText"] + userToken.actionMd5 = packetData["actionMd5"] + userToken.actionMods = packetData["actionMods"] + userToken.gameMode = packetData["gameMode"] + + # Enqueue our new user panel and stats to everyone + glob.tokens.enqueueAll(serverPackets.userPanel(userID)) + glob.tokens.enqueueAll(serverPackets.userStats(userID)) + + # Console output + print("> {} changed action: {} [{}][{}]".format(username, str(userToken.actionID), userToken.actionText, userToken.actionMd5)) diff --git a/changeMatchModsEvent.py b/changeMatchModsEvent.py new file mode 100644 index 0000000..fb4ee02 --- /dev/null +++ b/changeMatchModsEvent.py @@ -0,0 +1,43 @@ +import glob +import clientPackets +import matchModModes +import mods + +def handle(userToken, packetData): + # Get token data + userID = userToken.userID + + # Get packet data + packetData = clientPackets.changeMods(packetData) + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + match = glob.matches.matches[matchID] + + # Set slot or match mods according to modType + if match.matchModMode == matchModModes.freeMod: + # Freemod + + # Host can set global DT/HT + if userID == match.hostUserID: + # If host has selected DT/HT and Freemod is enabled, set DT/HT as match mod + if (packetData["mods"] & mods.DoubleTime) > 0: + match.changeMatchMods(mods.DoubleTime) + # Nighcore + if (packetData["mods"] & mods.Nightcore) > 0: + match.changeMatchMods(match.mods+mods.Nightcore) + elif (packetData["mods"] & mods.HalfTime) > 0: + match.changeMatchMods(mods.HalfTime) + else: + # No DT/HT, set global mods to 0 (we are in freemod mode) + match.changeMatchMods(0) + + # Set slot mods + slotID = match.getUserSlotID(userID) + if slotID != None: + match.setSlotMods(slotID, packetData["mods"]) + else: + # Not freemod, set match mods + match.changeMatchMods(packetData["mods"]) diff --git a/changeMatchPasswordEvent.py b/changeMatchPasswordEvent.py new file mode 100644 index 0000000..210d50f --- /dev/null +++ b/changeMatchPasswordEvent.py @@ -0,0 +1,17 @@ +import clientPackets +import glob + +def handle(userToken, packetData): + # Read packet data. Same structure as changeMatchSettings + packetData = clientPackets.changeMatchSettings(packetData) + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + + # Get our match + match = glob.matches.matches[matchID] + + # Update match password + match.changePassword(packetData["matchPassword"]) diff --git a/changeMatchSettingsEvent.py b/changeMatchSettingsEvent.py new file mode 100644 index 0000000..c82cda5 --- /dev/null +++ b/changeMatchSettingsEvent.py @@ -0,0 +1,109 @@ +import glob +import clientPackets +import matchModModes +import consoleHelper +import bcolors +import random +import matchTeamTypes +import matchTeams +import slotStatuses + +def handle(userToken, packetData): + # Read new settings + packetData = clientPackets.changeMatchSettings(packetData) + + # Get match ID + matchID = userToken.matchID + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Get match object + match = glob.matches.matches[matchID] + + # Some dank memes easter egg + memeTitles = [ + "RWC 2020", + "Fokabot is a duck", + "Dank memes", + "1337ms Ping", + "Iscriviti a Xenotoze", + "...e i marò?", + "Superman dies", + "The brace is on fire", + "print_foot()", + "#FREEZEBARKEZ", + "Ripple devs are actually cats", + "Thank Mr Shaural", + "NEVER GIVE UP", + "T I E D W I T H U N I T E D", + "HIGHEST HDHR LOBBY OF ALL TIME", + "This is gasoline and I set myself on fire", + "Everyone is cheating apparently", + "Kurwa mac", + "TATOE", + "This is not your drama landfill.", + "I like cheese", + "NYO IS NOT A CAT HE IS A DO(N)G", + "Datingu startuato" + ] + + # Set match name + match.matchName = packetData["matchName"] if packetData["matchName"] != "meme" else random.choice(memeTitles) + + # Update match settings + match.inProgress = packetData["inProgress"] + match.matchPassword = packetData["matchPassword"] + match.beatmapName = packetData["beatmapName"] + match.beatmapID = packetData["beatmapID"] + match.hostUserID = packetData["hostUserID"] + match.gameMode = packetData["gameMode"] + + oldBeatmapMD5 = match.beatmapMD5 + oldMods = match.mods + + match.mods = packetData["mods"] + match.beatmapMD5 = packetData["beatmapMD5"] + match.matchScoringType = packetData["scoringType"] + match.matchTeamType = packetData["teamType"] + match.matchModMode = packetData["freeMods"] + + # Reset ready if needed + if oldMods != match.mods or oldBeatmapMD5 != match.beatmapMD5: + for i in range(0,16): + if match.slots[i]["status"] == slotStatuses.ready: + match.slots[i]["status"] = slotStatuses.notReady + + # Reset mods if needed + if match.matchModMode == matchModModes.normal: + # Reset slot mods if not freeMods + for i in range(0,16): + match.slots[i]["mods"] = 0 + else: + # Reset match mods if freemod + match.mods = 0 + + # Set/reset teams + if match.matchTeamType == matchTeamTypes.teamVs or match.matchTeamType == matchTeamTypes.tagTeamVs: + # Set teams + c=0 + for i in range(0,16): + if match.slots[i]["team"] == matchTeams.noTeam: + match.slots[i]["team"] = matchTeams.red if c % 2 == 0 else matchTeams.blue + c+=1 + else: + # Reset teams + for i in range(0,16): + match.slots[i]["team"] = matchTeams.noTeam + + # Force no freemods if tag coop + if match.matchTeamType == matchTeamTypes.tagCoop or match.matchTeamType == matchTeamTypes.tagTeamVs: + match.matchModMode = matchModModes.normal + + # Send updated settings + match.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: Updated room settings".format(match.matchID), bcolors.BLUE) + #consoleHelper.printColored("> MPROOM{}: DEBUG: Host is {}".format(match.matchID, match.hostUserID), bcolors.PINK) diff --git a/changeSlotEvent.py b/changeSlotEvent.py new file mode 100644 index 0000000..b20b2db --- /dev/null +++ b/changeSlotEvent.py @@ -0,0 +1,18 @@ +import clientPackets +import glob +import consoleHelper +import bcolors + +def handle(userToken, packetData): + # Get usertoken data + userID = userToken.userID + username = userToken.username + + # Read packet data + packetData = clientPackets.changeSlot(packetData) + + # Get match + match = glob.matches.matches[userToken.matchID] + + # Change slot + match.userChangeSlot(userID, packetData["slotID"]) diff --git a/channel.py b/channel.py new file mode 100644 index 0000000..607e1d8 --- /dev/null +++ b/channel.py @@ -0,0 +1,78 @@ +class channel: + """ + A chat channel + + name -- channel name + description -- channel description + connectedUsers -- connected users IDs list + publicRead -- bool + publicWrite -- bool + moderated -- bool + """ + + name = "" + description = "" + connectedUsers = [] + + publicRead = False + publicWrite = False + moderated = False + + def __init__(self, __name, __description, __publicRead, __publicWrite): + """ + Create a new chat channel object + + __name -- channel name + __description -- channel description + __publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins + __publicWrite -- bool, same as public read but relative to write permissions + """ + + self.name = __name + self.description = __description + self.publicRead = __publicRead + self.publicWrite = __publicWrite + self.connectedUsers = [] + + + def userJoin(self, __userID): + """ + Add a user to connected users + + __userID -- user ID that joined the channel + """ + + if __userID not in self.connectedUsers: + self.connectedUsers.append(__userID) + + + def userPart(self, __userID): + """ + Remove a user from connected users + + __userID -- user ID that left the channel + """ + + connectedUsers = self.connectedUsers + if __userID in connectedUsers: + connectedUsers.remove(__userID) + + + def getConnectedUsers(self): + """ + Get connected user IDs list + + return -- connectedUsers list + """ + + return self.connectedUsers + + + def getConnectedUsersCount(self): + """ + Count connected users + + return -- connected users number + """ + + return len(self.connectedUsers) diff --git a/channelJoinEvent.py b/channelJoinEvent.py new file mode 100644 index 0000000..25aaf2b --- /dev/null +++ b/channelJoinEvent.py @@ -0,0 +1,56 @@ +""" +Event called when someone joins a channel +""" + +import clientPackets +import consoleHelper +import bcolors +import serverPackets +import glob +import exceptions + +def handle(userToken, packetData): + # Channel join packet + packetData = clientPackets.channelJoin(packetData) + joinChannel(userToken, packetData["channel"]) + +def joinChannel(userToken, channelName): + ''' + Join a channel + + userToken -- user token object of user that joins the chanlle + channelName -- name of channel + ''' + try: + # Get usertoken data + username = userToken.username + userID = userToken.userID + userRank = userToken.rank + + # Check spectator channel + # If it's spectator channel, skip checks and list stuff + if channelName != "#spectator" and channelName != "#multiplayer": + # Normal channel, do check stuff + # Make sure the channel exists + if channelName not in glob.channels.channels: + raise exceptions.channelUnknownException + + # Check channel permissions + if glob.channels.channels[channelName].publicRead == False and userRank <= 2: + raise exceptions.channelNoPermissionsException + + # Add our userID to users in that channel + glob.channels.channels[channelName].userJoin(userID) + + # Add the channel to our joined channel + userToken.joinChannel(channelName) + + # Send channel joined + userToken.enqueue(serverPackets.channelJoinSuccess(userID, channelName)) + + # Console output + consoleHelper.printColored("> {} joined channel {}".format(username, channelName), bcolors.GREEN) + except exceptions.channelNoPermissionsException: + consoleHelper.printColored("[!] {} attempted to join channel {}, but they have no read permissions".format(username, channelName), bcolors.RED) + except exceptions.channelUnknownException: + consoleHelper.printColored("[!] {} attempted to join an unknown channel ({})".format(username, channelName), bcolors.RED) diff --git a/channelList.py b/channelList.py new file mode 100644 index 0000000..e0e9a4b --- /dev/null +++ b/channelList.py @@ -0,0 +1,40 @@ +import glob +import channel + +class channelList: + """ + Channel list + + channels -- dictionary. key: channel name, value: channel object + """ + + channels = {} + + + def loadChannels(self): + """ + Load chat channels from db and add them to channels dictionary + """ + + # Get channels from DB + channels = glob.db.fetchAll("SELECT * FROM bancho_channels") + + # Add each channel if needed + for i in channels: + if i["name"] not in self.channels: + publicRead = True if i["public_read"] == 1 else False + publicWrite = True if i["public_write"] == 1 else False + self.addChannel(i["name"], i["description"], publicRead, publicWrite) + + + def addChannel(self, __name, __description, __publicRead, __publicWrite): + """ + Add a channel object to channels dictionary + + __name -- channel name + __description -- channel description + __publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins + __publicWrite -- bool, same as public read but relative to write permissions + """ + + self.channels[__name] = channel.channel(__name, __description, __publicRead, __publicWrite) diff --git a/channelPartEvent.py b/channelPartEvent.py new file mode 100644 index 0000000..9c34cd7 --- /dev/null +++ b/channelPartEvent.py @@ -0,0 +1,36 @@ +""" +Event called when someone parts a channel +""" + +import consoleHelper +import bcolors +import glob +import clientPackets +import serverPackets + +def handle(userToken, packetData): + # Channel part packet + packetData = clientPackets.channelPart(packetData) + partChannel(userToken, packetData["channel"]) + +def partChannel(userToken, channelName, kick = False): + # Get usertoken data + username = userToken.username + userID = userToken.userID + + # Remove us from joined users and joined channels + if channelName in glob.channels.channels: + # Check that user is in channel + if channelName in userToken.joinedChannels: + userToken.partChannel(channelName) + + # Check if user is in channel + if userID in glob.channels.channels[channelName].connectedUsers: + glob.channels.channels[channelName].userPart(userID) + + # Force close tab if needed + if kick == True: + userToken.enqueue(serverPackets.channelKicked(channelName)) + + # Console output + consoleHelper.printColored("> {} parted channel {}".format(username, channelName), bcolors.YELLOW) diff --git a/clientPackets.py b/clientPackets.py new file mode 100644 index 0000000..734cf1f --- /dev/null +++ b/clientPackets.py @@ -0,0 +1,143 @@ +""" Contains functions used to read specific client packets from byte stream """ +import dataTypes +import packetHelper +import slotStatuses + + +""" General packets """ +def userActionChange(stream): + return packetHelper.readPacketData(stream, + [ + ["actionID", dataTypes.byte], + ["actionText", dataTypes.string], + ["actionMd5", dataTypes.string], + ["actionMods", dataTypes.uInt32], + ["gameMode", dataTypes.byte] + ]) + + + +""" Client chat packets """ +def sendPublicMessage(stream): + return packetHelper.readPacketData(stream, + [ + ["unknown", dataTypes.string], + ["message", dataTypes.string], + ["to", dataTypes.string] + ]) + +def sendPrivateMessage(stream): + return packetHelper.readPacketData(stream, + [ + ["unknown", dataTypes.string], + ["message", dataTypes.string], + ["to", dataTypes.string], + ["unknown2", dataTypes.uInt32] + ]) + +def setAwayMessage(stream): + return packetHelper.readPacketData(stream, + [ + ["unknown", dataTypes.string], + ["awayMessage", dataTypes.string] + ]) + +def channelJoin(stream): + return packetHelper.readPacketData(stream,[["channel", dataTypes.string]]) + +def channelPart(stream): + return packetHelper.readPacketData(stream,[["channel", dataTypes.string]]) + +def addRemoveFriend(stream): + return packetHelper.readPacketData(stream, [["friendID", dataTypes.sInt32]]) + + + +""" SPECTATOR PACKETS """ +def startSpectating(stream): + return packetHelper.readPacketData(stream,[["userID", dataTypes.sInt32]]) + + +""" MULTIPLAYER PACKETS """ +def matchSettings(stream): + # Data to return, will be merged later + data = [] + + # Some settings + struct = [ + ["matchID", dataTypes.uInt16], + ["inProgress", dataTypes.byte], + ["unknown", dataTypes.byte], + ["mods", dataTypes.uInt32], + ["matchName", dataTypes.string], + ["matchPassword", dataTypes.string], + ["beatmapName", dataTypes.string], + ["beatmapID", dataTypes.uInt32], + ["beatmapMD5", dataTypes.string] + ] + + # Slot statuses (not used) + for i in range(0,16): + struct.append(["slot{}Status".format(str(i)), dataTypes.byte]) + + # Slot statuses (not used) + for i in range(0,16): + struct.append(["slot{}Team".format(str(i)), dataTypes.byte]) + + # Read first part + data.append(packetHelper.readPacketData(stream, struct)) + + # Skip userIDs because fuck + start = 7+2+1+1+4+4+16+16+len(data[0]["matchName"])+len(data[0]["matchPassword"])+len(data[0]["beatmapMD5"])+len(data[0]["beatmapName"]) + start += 1 if (data[0]["matchName"] == "") else 2 + start += 1 if (data[0]["matchPassword"] == "") else 2 + start += 2 # If beatmap name and MD5 don't change, the client sends \x0b\x00 istead of \x00 only, so always add 2. ...WHY! + start += 2 + for i in range(0,16): + s = data[0]["slot{}Status".format(str(i))] + if s != slotStatuses.free and s != slotStatuses.locked: + start += 4 + + # Other settings + struct = [ + ["hostUserID", dataTypes.sInt32], + ["gameMode", dataTypes.byte], + ["scoringType", dataTypes.byte], + ["teamType", dataTypes.byte], + ["freeMods", dataTypes.byte], + ] + + # Read last part + data.append(packetHelper.readPacketData(stream[start:], struct, False)) + + # Mods if freemod (not used) + #if data[1]["freeMods"] == 1: + + result = {} + for i in data: + result.update(i) + return result + +def createMatch(stream): + return matchSettings(stream) + +def changeMatchSettings(stream): + return matchSettings(stream) + +def changeSlot(stream): + return packetHelper.readPacketData(stream, [["slotID", dataTypes.uInt32]]) + +def joinMatch(stream): + return packetHelper.readPacketData(stream, [["matchID", dataTypes.uInt32], ["password", dataTypes.string]]) + +def changeMods(stream): + return packetHelper.readPacketData(stream, [["mods", dataTypes.uInt32]]) + +def lockSlot(stream): + return packetHelper.readPacketData(stream, [["slotID", dataTypes.uInt32]]) + +def transferHost(stream): + return packetHelper.readPacketData(stream, [["slotID", dataTypes.uInt32]]) + +def matchInvite(stream): + return packetHelper.readPacketData(stream, [["userID", dataTypes.uInt32]]) diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..f265d29 --- /dev/null +++ b/config.ini @@ -0,0 +1,24 @@ +[db] +host = localhost +username = root +password = meme +database = heidi +pingtime = 600 + +[server] +server = tornado +host = 0.0.0.0 +port = 5001 +outputpackets = 0 +outputrequesttime = 0 +localizeusers = 0 +timeouttime = 100 +timeoutlooptime = 100 + +[flask] +threaded = 1 +debug = 1 +logger = 0 + +[ci] +key=rippleburgrw15gofmustard diff --git a/config.py b/config.py new file mode 100644 index 0000000..b48ef44 --- /dev/null +++ b/config.py @@ -0,0 +1,107 @@ +import os +import configparser + +class config: + """ + config.ini object + + config -- list with ini data + default -- if true, we have generated a default config.ini + """ + + config = configparser.ConfigParser() + fileName = "" # config filename + default = True + + # Check if config.ini exists and load/generate it + def __init__(self, __file): + """ + Initialize a config object + + __file -- filename + """ + + self.fileName = __file + if os.path.isfile(self.fileName): + # config.ini found, load it + self.config.read(self.fileName) + self.default = False + else: + # config.ini not found, generate a default one + self.generateDefaultConfig() + self.default = True + + + # Check if config.ini has all needed the keys + def checkConfig(self): + """ + Check if this config has the required keys + + return -- True if valid, False if not + """ + + try: + # Try to get all the required keys + self.config.get("db","host") + self.config.get("db","username") + self.config.get("db","password") + self.config.get("db","database") + self.config.get("db","pingtime") + + self.config.get("server","server") + self.config.get("server","host") + self.config.get("server","port") + self.config.get("server","localizeusers") + self.config.get("server","outputpackets") + self.config.get("server","outputrequesttime") + self.config.get("server","timeouttime") + self.config.get("server","timeoutlooptime") + + if self.config["server"]["server"] == "flask": + # Flask only config + self.config.get("flask","threaded") + self.config.get("flask","debug") + self.config.get("flask","logger") + + self.config.get("ci","key") + return True + except: + return False + + + # Generate a default config.ini + def generateDefaultConfig(self): + """Open and set default keys for that config file""" + + # Open config.ini in write mode + f = open(self.fileName, "w") + + # Set keys to config object + self.config.add_section("db") + self.config.set("db", "host", "localhost") + self.config.set("db", "username", "root") + self.config.set("db", "password", "") + self.config.set("db", "database", "ripple") + self.config.set("db", "pingtime", "600") + + self.config.add_section("server") + self.config.set("server", "server", "tornado") + self.config.set("server", "host", "0.0.0.0") + self.config.set("server", "port", "5001") + self.config.set("server", "localizeusers", "1") + self.config.set("server", "outputpackets", "0") + self.config.set("server", "outputrequesttime", "0") + self.config.set("server", "timeoutlooptime", "100") + self.config.set("server", "timeouttime", "100") + + self.config.add_section("flask") + self.config.set("flask", "threaded", "1") + self.config.set("flask", "debug", "0") + self.config.set("flask", "logger", "0") + + self.config.add_section("ci") + self.config.set("ci", "key", "changeme") + + # Write ini to file and close + self.config.write(f) + f.close() diff --git a/consoleHelper.py b/consoleHelper.py new file mode 100644 index 0000000..3fcfb0b --- /dev/null +++ b/consoleHelper.py @@ -0,0 +1,71 @@ +"""Some console related functions""" + +import bcolors +import glob + +def printServerStartHeader(asciiArt): + """Print server start header with optional ascii art + + asciiArt -- if True, will print ascii art too""" + + if asciiArt == True: + print("{} _ __".format(bcolors.GREEN)) + print(" (_) / /") + print(" ______ __ ____ ____ / /____") + print(" / ___/ / _ \\/ _ \\/ / _ \\") + print(" / / / / /_) / /_) / / ____/") + print("/__/ /__/ .___/ .___/__/ \\_____/") + print(" / / / /") + print(" /__/ /__/\r\n") + print(" .. o .") + print(" o.o o . o") + print(" oo...") + print(" __[]__") + print(" nyo --> _\\:D/_/o_o_o_|__ u wot m8") + print(" \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/") + print(" \\ . .. .. . /") + print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{}".format(bcolors.ENDC)) + + printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN) + printColored("> Made by the Ripple team", bcolors.GREEN) + printColored("> {}https://github.com/osuripple/ripple".format(bcolors.UNDERLINE), bcolors.GREEN) + printColored("> Press CTRL+C to exit\n",bcolors.GREEN) + + +def printNoNl(string): + """ + Print string without new line at the end + + string -- string to print + """ + + print(string, end="") + + +def printColored(string, color): + """ + Print colored string + + string -- string to print + color -- see bcolors.py + """ + + print("{}{}{}".format(color, string, bcolors.ENDC)) + + +def printError(): + """Print error text FOR LOADING""" + + printColored("Error", bcolors.RED) + + +def printDone(): + """Print error text FOR LOADING""" + + printColored("Done", bcolors.GREEN) + + +def printWarning(): + """Print error text FOR LOADING""" + + printColored("Warning", bcolors.YELLOW) diff --git a/countryHelper.py b/countryHelper.py new file mode 100644 index 0000000..d5808d3 --- /dev/null +++ b/countryHelper.py @@ -0,0 +1,282 @@ +"""Contains all country codes with their osu numeric code""" + +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 +} + + +def getCountryID(code): + """ + Get country ID for osu client + + code -- country name abbreviation (eg: US) + return -- country code int + """ + + if code in countryCodes: + return countryCodes[code] + else: + return 0 + +def getCountryLetters(code): + """ + Get country letters from osu country ID + + code -- country code int + return -- country name (2 letters) (XX if code not found) + """ + + for key, value in countryCodes.items(): + if value == code: + return key + + return "XX" diff --git a/createMatchEvent.py b/createMatchEvent.py new file mode 100644 index 0000000..3eca989 --- /dev/null +++ b/createMatchEvent.py @@ -0,0 +1,44 @@ +import serverPackets +import clientPackets +import glob +import consoleHelper +import bcolors +import joinMatchEvent +import exceptions + +def handle(userToken, packetData): + try: + # get usertoken data + userID = userToken.userID + + # Read packet data + packetData = clientPackets.createMatch(packetData) + + # Create a match object + # TODO: Player number check + matchID = glob.matches.createMatch(packetData["matchName"], packetData["matchPassword"], packetData["beatmapID"], packetData["beatmapName"], packetData["beatmapMD5"], packetData["gameMode"], userID) + + # Make sure the match has been created + if matchID not in glob.matches.matches: + raise exceptions.matchCreateError + + # Get match object + match = glob.matches.matches[matchID] + + # Join that match + joinMatchEvent.joinMatch(userToken, matchID, packetData["matchPassword"]) + + # Give host to match creator + match.setHost(userID) + + # Send match create packet to everyone in lobby + for i in glob.matches.usersInLobby: + # Make sure this user is still connected + token = glob.tokens.getTokenFromUserID(i) + if token != None: + token.enqueue(serverPackets.createMatch(matchID)) + + # Console output + consoleHelper.printColored("> MPROOM{}: Room created!".format(matchID), bcolors.BLUE) + except exceptions.matchCreateError: + consoleHelper.printColored("[!] Error while creating match!", bcolors.RED) diff --git a/crypt.py b/crypt.py new file mode 100644 index 0000000..0ec8edc --- /dev/null +++ b/crypt.py @@ -0,0 +1,302 @@ +# Huge thanks to Cairnarvon +# https://gist.github.com/Cairnarvon/5075687 + +# Initial permutation +IP = ( + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, +) + +# Final permutation, FP = IP^(-1) +FP = ( + 40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, +) + +# Permuted-choice 1 from the key bits to yield C and D. +# Note that bits 8,16... are left out: They are intended for a parity check. +PC1_C = ( + 57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, +) +PC1_D = ( + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4, +) + +# Permuted-choice 2, to pick out the bits from the CD array that generate the +# key schedule. +PC2_C = ( + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, +) +PC2_D = ( + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32, +) + +# The C and D arrays are used to calculate the key schedule. +C = [0] * 28 +D = [0] * 28 + +# The key schedule. Generated from the key. +KS = [[0] * 48 for _ in range(16)] + +# The E bit-selection table. +E = [0] * 48 +e2 = ( + 32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1, +) + +# S-boxes. +S = ( + ( + 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13 + ), + ( + 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9 + ), + ( + 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12 + ), + ( + 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14 + ), + ( + 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3 + ), + ( + 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13 + ), + ( + 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12 + ), + ( + 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11 + ) +) + +# P is a permutation on the selected combination of the current L and key. +P = ( + 16, 7, 20, 21, + 29, 12, 28, 17, + 1, 15, 23, 26, + 5, 18, 31, 10, + 2, 8, 24, 14, + 32, 27, 3, 9, + 19, 13, 30, 6, + 22, 11, 4, 25, +) + +# The combination of the key and the input, before selection. +preS = [0] * 48 + + +def __setkey(key): + """ + Set up the key schedule from the encryption key. + """ + global C, D, KS, E + + shifts = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1) + + # First, generate C and D by permuting the key. The lower order bit of each + # 8-bit char is not used, so C and D are only 28 bits apiece. + for i in range(28): + C[i] = key[PC1_C[i] - 1] + D[i] = key[PC1_D[i] - 1] + + for i in range(16): + # rotate + for k in range(shifts[i]): + temp = C[0] + + for j in range(27): + C[j] = C[j + 1] + + C[27] = temp + temp = D[0] + for j in range(27): + D[j] = D[j + 1] + + D[27] = temp + + # get Ki. Note C and D are concatenated + for j in range(24): + KS[i][j] = C[PC2_C[j] - 1] + KS[i][j + 24] = D[PC2_D[j] - 28 - 1] + + # load E with the initial E bit selections + for i in range(48): + E[i] = e2[i] + +def __encrypt(block): + global preS + + left, right = [], [] # block in two halves + f = [0] * 32 + + # First, permute the bits in the input + for j in range(32): + left.append(block[IP[j] - 1]) + + for j in range(32, 64): + right.append(block[IP[j] - 1]) + + # Perform an encryption operation 16 times. + for i in range(16): + # Save the right array, which will be the new left. + old = right[:] + + # Expand right to 48 bits using the E selector and exclusive-or with + # the current key bits. + for j in range(48): + preS[j] = right[E[j] - 1] ^ KS[i][j] + + # The pre-select bits are now considered in 8 groups of 6 bits each. + # The 8 selection functions map these 6-bit quantities into 4-bit + # quantities and the results are permuted to make an f(R, K). + # The indexing into the selection functions is peculiar; it could be + # simplified by rewriting the tables. + for j in range(8): + temp = 6 * j + k = S[j][(preS[temp + 0] << 5) + + (preS[temp + 1] << 3) + + (preS[temp + 2] << 2) + + (preS[temp + 3] << 1) + + (preS[temp + 4] << 0) + + (preS[temp + 5] << 4)] + + temp = 4 * j + f[temp + 0] = (k >> 3) & 1 + f[temp + 1] = (k >> 2) & 1 + f[temp + 2] = (k >> 1) & 1 + f[temp + 3] = (k >> 0) & 1 + + # The new right is left ^ f(R, K). + # The f here has to be permuted first, though. + for j in range(32): + right[j] = left[j] ^ f[P[j] - 1] + + # Finally the new left (the original right) is copied back. + left = old + + # The output left and right are reversed. + left, right = right, left + + # The final output gets the inverse permutation of the very original + for j in range(64): + i = FP[j] + if i < 33: + block[j] = left[i - 1] + else: + block[j] = right[i - 33] + + return block + +def crypt(pw, salt): + iobuf = [] + + # break pw into 64 bits + block = [] + for c in pw: + c = ord(c) + for j in range(7): + block.append((c >> (6 - j)) & 1) + block.append(0) + block += [0] * (64 - len(block)) + + # set key based on pw + __setkey(block) + + for i in range(2): + # store salt at beginning of results + iobuf.append(salt[i]) + c = ord(salt[i]) + + if c > ord('Z'): + c -= 6 + + if c > ord('9'): + c -= 7 + + c -= ord('.') + + # use salt to effect the E-bit selection + for j in range(6): + if (c >> j) & 1: + E[6 * i + j], E[6 * i + j + 24] = E[6 * i + j + 24], E[6 * i + j] + + # call DES encryption 25 times using pw as key and initial data = 0 + block = [0] * 66 + for i in range(25): + block = __encrypt(block) + + # format encrypted block for standard crypt(3) output + for i in range(11): + c = 0 + for j in range(6): + c <<= 1 + c |= block[6 * i + j] + + c += ord('.') + if c > ord('9'): + c += 7 + + if c > ord('Z'): + c += 6 + + iobuf.append(chr(c)) + + return ''.join(iobuf) diff --git a/dataTypes.py b/dataTypes.py new file mode 100644 index 0000000..f391e26 --- /dev/null +++ b/dataTypes.py @@ -0,0 +1,12 @@ +"""Bancho packets data types""" +#TODO: Uppercase, maybe? +byte = 0 +uInt16 = 1 +sInt16 = 2 +uInt32 = 3 +sInt32 = 4 +uInt64 = 5 +sInt64 = 6 +string = 7 +ffloat = 8 # because float is a keyword +bbytes = 9 diff --git a/databaseHelper.py b/databaseHelper.py new file mode 100644 index 0000000..711bcc9 --- /dev/null +++ b/databaseHelper.py @@ -0,0 +1,137 @@ +import pymysql +import bcolors +import consoleHelper +import threading + +class db: + """A MySQL database connection""" + + connection = None + disconnected = False + pingTime = 600 + + def __init__(self, __host, __username, __password, __database, __pingTime = 600): + """ + Connect to MySQL database + + __host -- MySQL host name + __username -- MySQL username + __password -- MySQL password + __database -- MySQL database name + __pingTime -- MySQL database ping time (default: 600) + """ + + self.connection = pymysql.connect(host=__host, user=__username, password=__password, db=__database, cursorclass=pymysql.cursors.DictCursor, autocommit=True) + self.pingTime = __pingTime + self.pingLoop() + + + def bindParams(self, __query, __params): + """ + Replace every ? with the respective **escaped** parameter in array + + __query -- query with ?s + __params -- array with params + + return -- new query + """ + + for i in __params: + escaped = self.connection.escape(i) + __query = __query.replace("?", str(escaped), 1) + + return __query + + + def execute(self, __query, __params = None): + """ + Execute a SQL query + + __query -- query, can contain ?s + __params -- array with params. Optional + """ + + + with self.connection.cursor() as cursor: + try: + # Bind params if needed + if __params != None: + __query = self.bindParams(__query, __params) + + # Execute the query + cursor.execute(__query) + finally: + # Close this connection + cursor.close() + + + def fetch(self, __query, __params = None, __all = False): + """ + Fetch the first (or all) element(s) of SQL query result + + __query -- query, can contain ?s + __params -- array with params. Optional + __all -- if true, will fetch all values. Same as fetchAll + + return -- dictionary with result data or False if failed + """ + + + with self.connection.cursor() as cursor: + try: + # Bind params if needed + if __params != None: + __query = self.bindParams(__query, __params) + + # Execute the query with binded params + cursor.execute(__query) + + # Get first result and return it + if __all == False: + return cursor.fetchone() + else: + return cursor.fetchall() + finally: + # Close this connection + cursor.close() + + + def fetchAll(self, __query, __params = None): + """ + Fetch the all elements of SQL query result + + __query -- query, can contain ?s + __params -- array with params. Optional + + return -- dictionary with result data + """ + + return self.fetch(__query, __params, True) + + def pingLoop(self): + """ + Pings MySQL server. We need to ping/execute a query at least once every 8 hours + or the connection will die. + If called once, will recall after 30 minutes and so on, forever + CALL THIS FUNCTION ONLY ONCE! + """ + + # Default loop time + time = self.pingTime + + # Make sure the connection is alive + try: + # Try to ping and reconnect if not connected + self.connection.ping() + if self.disconnected == True: + # If we were disconnected, set disconnected to false and print message + self.disconnected = False + consoleHelper.printColored("> Reconnected to MySQL server!", bcolors.GREEN) + except: + # Can't ping MySQL server. Show error and call loop in 5 seconds + consoleHelper.printColored("[!] CRITICAL!! MySQL connection died! Make sure your MySQL server is running! Checking again in 5 seconds...", bcolors.RED) + self.disconnected = True + time = 5 + + # Schedule a new check (endless loop) + threading.Timer(time, self.pingLoop).start() diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..4035405 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,58 @@ +"""Bancho exceptions""" +# TODO: Prints in exceptions +class loginFailedException(Exception): + pass + +class loginBannedException(Exception): + pass + +class tokenNotFoundException(Exception): + pass + +class channelNoPermissionsException(Exception): + pass + +class channelUnknownException(Exception): + pass + +class channelModeratedException(Exception): + pass + +class noAdminException(Exception): + pass + +class commandSyntaxException(Exception): + pass + +class banchoConfigErrorException(Exception): + pass + +class banchoMaintenanceException(Exception): + pass + +class moderatedPMException(Exception): + pass + +class userNotFoundException(Exception): + pass + +class alreadyConnectedException(Exception): + pass + +class stopSpectating(Exception): + pass + +class matchWrongPasswordException(Exception): + pass + +class matchNotFoundException(Exception): + pass + +class matchJoinErrorException(Exception): + pass + +class matchCreateError(Exception): + pass + +class banchoRestartingException(Exception): + pass diff --git a/fokabot.py b/fokabot.py new file mode 100644 index 0000000..169668c --- /dev/null +++ b/fokabot.py @@ -0,0 +1,55 @@ +"""FokaBot related functions""" +import userHelper +import glob +import actions +import serverPackets +import fokabotCommands + +def connect(): + """Add FokaBot to connected users and send userpanel/stats packet to everyone""" + + token = glob.tokens.addToken(999) + token.actionID = actions.idle + glob.tokens.enqueueAll(serverPackets.userPanel(999)) + glob.tokens.enqueueAll(serverPackets.userStats(999)) + +def disconnect(): + """Remove FokaBot from connected users""" + + glob.tokens.deleteToken(glob.tokens.getTokenFromUserID(999)) + +def fokabotResponse(fro, chan, message): + """ + Check if a message has triggered fokabot (and return its response) + + fro -- sender username (for permissions stuff with admin commands) + chan -- channel name + message -- message + + return -- fokabot's response string or False + """ + + for i in fokabotCommands.commands: + # Loop though all commands + if i["trigger"] in message: + # message has triggered a command + + # Make sure the user has right permissions + if i["minRank"] > 1: + # Get rank from db only if minrank > 1, so we save some CPU + if userHelper.getRankPrivileges(userHelper.getID(fro)) < i["minRank"]: + return False + + # Check argument number + message = message.split(" ") + if i["syntax"] != "" and len(message) <= len(i["syntax"].split(" ")): + return "Wrong syntax: {} {}".format(i["trigger"], i["syntax"]) + + # Return response or execute callback + if i["callback"] == None: + return i["response"] + else: + return i["callback"](fro, chan, message[1:]) + + # No commands triggered + return False diff --git a/fokabotCommands.py b/fokabotCommands.py new file mode 100644 index 0000000..0bdd615 --- /dev/null +++ b/fokabotCommands.py @@ -0,0 +1,355 @@ +import fokabot +import random +import glob +import serverPackets +import exceptions +import userHelper +import time +import systemHelper + +""" +Commands callbacks + +Must have fro, chan and messages as arguments +fro -- name of who triggered the command +chan -- channel where the message was sent +message -- list containing arguments passed from the message + [0] = first argument + [1] = second argument + . . . + +return the message or **False** if there's no response by the bot +""" + +def faq(fro, chan, message): + if message[0] == "rules": + return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]." + elif message[0] == "rules": + return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]." + elif message[0] == "swearing": + return "Please don't abuse swearing" + elif message[0] == "spam": + return "Please don't spam" + elif message[0] == "offend": + return "Please don't offend other players" + elif message[0] == "github": + return "(Ripple's Github page!)[https://github.com/osuripple/ripple]" + elif message[0] == "discord": + return "(Join Ripple's Discord!)[https://discord.gg/0rJcZruIsA6rXuIx]" + elif message[0] == "blog": + return "You can find the latest Ripple news on the (blog)[https://ripple.moe/blog/]!" + elif message[0] == "changelog": + return "Check the (changelog)[https://ripple.moe/index.php?p=17] !" + elif message[0] == "status": + return "Check the server status (here!)[https://ripple.moe/index.php?p=27]" + +def roll(fro, chan, message): + maxPoints = 100 + if len(message) >= 1: + if message[0].isdigit() == True and int(message[0]) > 0: + maxPoints = int(message[0]) + + points = random.randrange(0,maxPoints) + return "{} rolls {} points!".format(fro, str(points)) + +def ask(fro, chan, message): + return random.choice(["yes", "no", "maybe"]) + +def alert(fro, chan, message): + glob.tokens.enqueueAll(serverPackets.notification(' '.join(message[:]))) + return False + +def moderated(fro, chan, message): + try: + # Make sure we are in a channel and not PM + if chan.startswith("#") == False: + raise exceptions.moderatedPMException + + # Get on/off + enable = True + if len(message) >= 1: + if message[0] == "off": + enable = False + + # Turn on/off moderated mode + glob.channels.channels[chan].moderated = enable + return "This channel is {} in moderated mode!".format("now" if enable else "no longer") + except exceptions.moderatedPMException: + return "You are trying to put a private chat in moderated mode. Are you serious?!? You're fired." + +def kickAll(fro, chan, message): + # Kick everyone but mods/admins + toKick = [] + for key, value in glob.tokens.tokens.items(): + if value.rank < 3: + toKick.append(key) + + # Loop though users to kick (we can't change dictionary size while iterating) + for i in toKick: + if i in glob.tokens.tokens: + glob.tokens.tokens[i].kick() + + return "Whoops! Rip everyone." + +def kick(fro, chan, message): + # Get parameters + target = message[0].replace("_", " ") + + # Get target token and make sure is connected + targetToken = glob.tokens.getTokenFromUsername(target) + if targetToken == None: + return "{} is not online".format(target) + + # Kick user + targetToken.kick() + + # Bot response + return "{} has been kicked from the server.".format(target) + +def fokabotReconnect(fro, chan, message): + # Check if fokabot is already connected + if glob.tokens.getTokenFromUserID(999) != None: + return"Fokabot is already connected to Bancho" + + # Fokabot is not connected, connect it + fokabot.connect() + return False + +def silence(fro, chan, message): + for i in message: + i = i.lower() + target = message[0].replace("_", " ") + amount = message[1] + unit = message[2] + reason = ' '.join(message[3:]) + + # Get target user ID + targetUserID = userHelper.getID(target) + + # Make sure the user exists + if targetUserID == False: + return "{}: user not found".format(target) + + # Calculate silence seconds + if unit == 's': + silenceTime = int(amount) + elif unit == 'm': + silenceTime = int(amount)*60 + elif unit == 'h': + silenceTime = int(amount)*3600 + elif unit == 'd': + silenceTime = int(amount)*86400 + else: + return "Invalid time unit (s/m/h/d)." + + # Max silence time is 7 days + if silenceTime > 604800: + return "Invalid silence time. Max silence time is 7 days." + + # Calculate silence end time + endTime = int(time.time())+silenceTime + + # Update silence end in db + userHelper.silence(targetUserID, endTime, reason) + + # Send silence packet to target if he's connected + targetToken = glob.tokens.getTokenFromUsername(target) + if targetToken != None: + targetToken.enqueue(serverPackets.silenceEndTime(silenceTime)) + + return "{} has been silenced for the following reason: {}".format(target, reason) + +def removeSilence(fro, chan, message): + # Get parameters + for i in message: + i = i.lower() + target = message[0].replace("_", " ") + + # Make sure the user exists + targetUserID = userHelper.getID(target) + if targetUserID == False: + return "{}: user not found".format(target) + + # Reset user silence time and reason in db + userHelper.silence(targetUserID, 0, "") + + # Send new silence end packet to user if he's online + targetToken = glob.tokens.getTokenFromUsername(target) + if targetToken != None: + targetToken.enqueue(serverPackets.silenceEndTime(0)) + + return "{}'s silence reset".format(target) + +def restartShutdown(restart): + """Restart (if restart = True) or shutdown (if restart = False) pep.py safely""" + msg = "We are performing some maintenance. Bancho will {} in 5 seconds. Thank you for your patience.".format("restart" if restart else "shutdown") + systemHelper.scheduleShutdown(5, restart, msg) + return msg + +def systemRestart(fro, chan, message): + return restartShutdown(True) + +def systemShutdown(fro, chan, message): + return restartShutdown(False) + +def systemReload(fro, chan, message): + #Reload settings from bancho_settings + glob.banchoConf.loadSettings() + + # Reload channels too + glob.channels.loadChannels() + + # Send new channels and new bottom icon to everyone + glob.tokens.enqueueAll(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) + glob.tokens.enqueueAll(serverPackets.channelInfoEnd()) + for key, _ in glob.channels.channels.items(): + glob.tokens.enqueueAll(serverPackets.channelInfo(key)) + + return "Bancho settings reloaded!" + +def systemMaintenance(fro, chan, message): + # Turn on/off bancho maintenance + maintenance = True + + # Get on/off + if len(message) >= 2: + if message[1] == "off": + maintenance = False + + # Set new maintenance value in bancho_settings table + glob.banchoConf.setMaintenance(maintenance) + + if maintenance == True: + # We have turned on maintenance mode + # Users that will be disconnected + who = [] + + # Disconnect everyone but mod/admins + for _, value in glob.tokens.tokens.items(): + if value.rank < 3: + who.append(value.userID) + + glob.tokens.enqueueAll(serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.")) + glob.tokens.multipleEnqueue(serverPackets.loginError(), who) + msg = "The server is now in maintenance mode!" + else: + # We have turned off maintenance mode + # Send message if we have turned off maintenance mode + msg = "The server is no longer in maintenance mode!" + + # Chat output + return msg + +def systemStatus(fro, chan, message): + # Print some server info + data = systemHelper.getSystemInfo() + + # Final message + msg = "=== PEP.PY STATS ===\n" + msg += "Running pep.py server\n" + msg += "Webserver: {}\n".format(data["webServer"]) + msg += "\n" + msg += "=== BANCHO STATS ===\n" + msg += "Connected users: {}\n".format(str(data["connectedUsers"])) + msg += "\n" + msg += "=== SYSTEM STATS ===\n" + msg += "CPU: {}%\n".format(str(data["cpuUsage"])) + msg += "RAM: {}GB/{}GB\n".format(str(data["usedMemory"]), str(data["totalMemory"])) + if data["unix"] == True: + msg += "Load average: {}/{}/{}\n".format(str(data["loadAverage"][0]), str(data["loadAverage"][1]), str(data["loadAverage"][2])) + + return msg + +""" +Commands list + +trigger: message that triggers the command +callback: function to call when the command is triggered. Optional. +response: text to return when the command is triggered. Optional. +syntax: command syntax. Arguments must be separated by spaces (eg: ) +minRank: minimum rank to execute that command. Optional (default = 1) + +You MUST set trigger and callback/response, or the command won't work. +""" +commands = [ + { + "trigger": "!roll", + "callback": roll + }, { + "trigger": "!faq", + "syntax": "", + "callback": faq + }, { + "trigger": "!report", + "response": "Report command isn't here yet :c" + }, { + "trigger": "!help", + "response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for FokaBot's full command list" + }, { + "trigger": "!ask", + "syntax": "", + "callback": ask + }, { + "trigger": "!mm00", + "response": random.choice(["meme", "MA MAURO ESISTE?"]) + }, { + "trigger": "!alert", + "syntax": "", + "minRank": 4, + "callback": alert + }, { + "trigger": "!moderated", + "minRank": 3, + "callback": moderated + }, { + "trigger": "!kickall", + "minRank": 4, + "callback": kickAll + }, { + "trigger": "!kick", + "syntax": "", + "minRank": 3, + "callback": kick + }, { + "trigger": "!fokabot reconnect", + "minRank": 3, + "callback": fokabotReconnect + }, { + "trigger": "!silence", + "syntax": " ", + "minRank": 3, + "callback": silence + }, { + "trigger": "!removesilence", + "syntax": "", + "minRank": 3, + "callback": removeSilence + }, { + "trigger": "!system restart", + "minRank": 4, + "callback": systemRestart + }, { + "trigger": "!system shutdown", + "minRank": 4, + "callback": systemShutdown + }, { + "trigger": "!system reload", + "minRank": 3, + "callback": systemReload + }, { + "trigger": "!system maintenance", + "minRank": 3, + "callback": systemMaintenance + }, { + "trigger": "!system status", + "minRank": 3, + "callback": systemStatus + } +] + +# Commands list default values +for cmd in commands: + cmd.setdefault("syntax", "") + cmd.setdefault("minRank", 1) + cmd.setdefault("callback", None) + cmd.setdefault("response", "u w0t m8?") diff --git a/friendAddEvent.py b/friendAddEvent.py new file mode 100644 index 0000000..9ccf5dd --- /dev/null +++ b/friendAddEvent.py @@ -0,0 +1,10 @@ +import userHelper +import clientPackets + +def handle(userToken, packetData): + # Friend add packet + packetData = clientPackets.addRemoveFriend(packetData) + userHelper.addFriend(userToken.userID, packetData["friendID"]) + + # Console output + print("> {} have added {} to their friends".format(userToken.username, str(packetData["friendID"]))) diff --git a/friendRemoveEvent.py b/friendRemoveEvent.py new file mode 100644 index 0000000..450d369 --- /dev/null +++ b/friendRemoveEvent.py @@ -0,0 +1,10 @@ +import userHelper +import clientPackets + +def handle(userToken, packetData): + # Friend remove packet + packetData = clientPackets.addRemoveFriend(packetData) + userHelper.removeFriend(userToken.userID, packetData["friendID"]) + + # Console output + print("> {} have removed {} from their friends".format(userToken.username, str(packetData["friendID"]))) diff --git a/gameModes.py b/gameModes.py new file mode 100644 index 0000000..8716996 --- /dev/null +++ b/gameModes.py @@ -0,0 +1,23 @@ +"""Contains readable gamemodes with their codes""" +std = 0 +taiko = 1 +ctb = 2 +mania = 3 + +def getGameModeForDB(gameMode): + """ + Convert a gamemode number to string for database table/column + + gameMode -- gameMode int or variable (ex: gameMode.std) + + return -- game mode readable string for db + """ + + if gameMode == std: + return "std" + elif gameMode == taiko: + return "taiko" + elif gameMode == ctb: + return "ctb" + else: + return "mania" diff --git a/generalFunctions.py b/generalFunctions.py new file mode 100644 index 0000000..6fd96d6 --- /dev/null +++ b/generalFunctions.py @@ -0,0 +1,22 @@ +"""Some functions that don't fit in any other file""" + +def stringToBool(s): + """ + Convert a string (True/true/1) to bool + + s -- string/int value + return -- True/False + """ + + return (s == "True" or s== "true" or s == "1" or s == 1) + + +def hexString(s): + """ + Output s' bytes in HEX + + s -- string + return -- string with hex value + """ + + return ":".join("{:02x}".format(ord(c)) for c in s) diff --git a/glob.py b/glob.py new file mode 100644 index 0000000..42a6f93 --- /dev/null +++ b/glob.py @@ -0,0 +1,16 @@ +"""Global objects and variables""" + +import tokenList +import channelList +import matchList + +VERSION = "0.9" + +db = None +conf = None +banchoConf = None +tokens = tokenList.tokenList() +channels = channelList.channelList() +matches = matchList.matchList() +memes = True +restarting = False diff --git a/joinLobbyEvent.py b/joinLobbyEvent.py new file mode 100644 index 0000000..8f639c6 --- /dev/null +++ b/joinLobbyEvent.py @@ -0,0 +1,19 @@ +import serverPackets +import glob +import consoleHelper +import bcolors + +def handle(userToken, _): + # Get userToken data + username = userToken.username + userID = userToken.userID + + # Add user to users in lobby + glob.matches.lobbyUserJoin(userID) + + # Send matches data + for key, _ in glob.matches.matches.items(): + userToken.enqueue(serverPackets.createMatch(key)) + + # Console output + consoleHelper.printColored("> {} has joined multiplayer lobby".format(username), bcolors.BLUE) diff --git a/joinMatchEvent.py b/joinMatchEvent.py new file mode 100644 index 0000000..a0e0bb9 --- /dev/null +++ b/joinMatchEvent.py @@ -0,0 +1,60 @@ +import clientPackets +import serverPackets +import glob +import consoleHelper +import bcolors +import exceptions + +def handle(userToken, packetData): + # read packet data + packetData = clientPackets.joinMatch(packetData) + + # Get match from ID + joinMatch(userToken, packetData["matchID"], packetData["password"]) + + +def joinMatch(userToken, matchID, password): + try: + # TODO: leave other matches + # TODO: Stop spectating + + # get usertoken data + userID = userToken.userID + username = userToken.username + + # Make sure the match exists + if matchID not in glob.matches.matches: + raise exceptions.matchNotFoundException + + # Match exists, get object + match = glob.matches.matches[matchID] + + # Check password + # TODO: Admins can enter every match + if match.matchPassword != "": + if match.matchPassword != password: + raise exceptions.matchWrongPasswordException + + # Password is correct, join match + result = match.userJoin(userID) + + # Check if we've joined the match successfully + if result == False: + raise exceptions.matchJoinErrorException + + # Match joined, set matchID for usertoken + userToken.joinMatch(matchID) + + # Send packets + userToken.enqueue(serverPackets.matchJoinSuccess(matchID)) + userToken.enqueue(serverPackets.channelJoinSuccess(userID, "#multiplayer")) + userToken.enqueue(serverPackets.sendMessage("FokaBot", "#multiplayer", "Hi {}, and welcome to Ripple's multiplayer mode! This feature is still WIP and might have some issues. If you find any bugs, please report them (by clicking here)[https://ripple.moe/index.php?p=22].".format(username))) + except exceptions.matchNotFoundException: + userToken.enqueue(serverPackets.matchJoinFail()) + consoleHelper.printColored("[!] {} has tried to join a mp room, but it doesn't exist".format(userToken.username), bcolors.RED) + except exceptions.matchWrongPasswordException: + userToken.enqueue(serverPackets.matchJoinFail()) + consoleHelper.printColored("[!] {} has tried to join a mp room, but he typed the wrong password".format(userToken.username), bcolors.RED) + except exceptions.matchJoinErrorException: + userToken.enqueue(serverPackets.matchJoinFail()) + consoleHelper.printColored("[!] {} has tried to join a mp room, but an error has occured".format(userToken.username), bcolors.RED) diff --git a/locationHelper.py b/locationHelper.py new file mode 100644 index 0000000..1c09602 --- /dev/null +++ b/locationHelper.py @@ -0,0 +1,48 @@ +import urllib.request +import json + +import consoleHelper +import bcolors + +# API URL +url = "http://ip.zxq.co/" + + +def getCountry(ip): + """ + Get country from IP address + + ip -- IP Address + return -- Country code (2 letters) + """ + + # Default value, sent if API is memeing + country = "XX" + + try: + # Try to get country from Pikolo Aul's Go-Sanic ip API + country = json.loads(urllib.request.urlopen("{}/{}".format(url, ip)).read().decode())["country"] + except: + consoleHelper.printColored("[!] Error in get country", bcolors.RED) + + return country + + +def getLocation(ip): + """ + Get latitude and longitude from IP address + + ip -- IP address + return -- [latitude, longitude] + """ + + # Default value, sent if API is memeing + data = [0,0] + + try: + # Try to get position from Pikolo Aul's Go-Sanic ip API + data = json.loads(urllib.request.urlopen("{}/{}".format(url, ip)).read().decode())["loc"].split(",") + except: + consoleHelper.printColored("[!] Error in get position", bcolors.RED) + + return [float(data[0]), float(data[1])] diff --git a/loginEvent.py b/loginEvent.py new file mode 100644 index 0000000..43ee8bb --- /dev/null +++ b/loginEvent.py @@ -0,0 +1,172 @@ +import userHelper +import serverPackets +import exceptions +import glob +import consoleHelper +import bcolors +import locationHelper +import countryHelper +import time +import generalFunctions +import channelJoinEvent + +def handle(flaskRequest): + # Data to return + responseTokenString = "ayy" + responseData = bytes() + + # Get IP from flask request + requestIP = flaskRequest.headers.get('X-Real-IP') + if requestIP == None: + requestIP = flaskRequest.remote_addr + + # Console output + print("> Accepting connection from {}...".format(requestIP)) + + # Split POST body so we can get username/password/hardware data + # 2:-3 thing is because requestData has some escape stuff that we don't need + loginData = str(flaskRequest.data)[2:-3].split("\\n") + + # Process login + print("> Processing login request for {}...".format(loginData[0])) + try: + # If true, print error to console + err = False + + # Try to get the ID from username + userID = userHelper.getID(str(loginData[0])) + + if userID == False: + # Invalid username + raise exceptions.loginFailedException() + if userHelper.checkLogin(userID, loginData[1]) == False: + # Invalid password + raise exceptions.loginFailedException() + + # Make sure we are not banned + userAllowed = userHelper.getAllowed(userID) + if userAllowed == 0: + # Banned + raise exceptions.loginBannedException() + + # No login errors! + # Delete old tokens for that user and generate a new one + glob.tokens.deleteOldTokens(userID) + responseToken = glob.tokens.addToken(userID) + responseTokenString = responseToken.token + + # Get silence end + userSilenceEnd = max(0, userHelper.getSilenceEnd(userID)-int(time.time())) + + # Get supporter/GMT + userRank = userHelper.getRankPrivileges(userID) + userGMT = False + userSupporter = True + if userRank >= 3: + userGMT = True + + # Server restarting check + if glob.restarting == True: + raise exceptions.banchoRestartingException() + + # Maintenance check + if glob.banchoConf.config["banchoMaintenance"] == True: + if userGMT == False: + # We are not mod/admin, delete token, send notification and logout + glob.tokens.deleteToken(responseTokenString) + raise exceptions.banchoMaintenanceException() + else: + # We are mod/admin, send warning notification and continue + responseToken.enqueue(serverPackets.notification("Bancho is in maintenance mode. Only mods/admins have full access to the server.\nType !system maintenance off in chat to turn off maintenance mode.")) + + # Send all needed login packets + responseToken.enqueue(serverPackets.silenceEndTime(userSilenceEnd)) + responseToken.enqueue(serverPackets.userID(userID)) + responseToken.enqueue(serverPackets.protocolVersion()) + responseToken.enqueue(serverPackets.userSupporterGMT(userSupporter, userGMT)) + responseToken.enqueue(serverPackets.userPanel(userID)) + responseToken.enqueue(serverPackets.userStats(userID)) + + # Channel info end (before starting!?! wtf bancho?) + responseToken.enqueue(serverPackets.channelInfoEnd()) + + # Default opened channels + # TODO: Configurable default channels + channelJoinEvent.joinChannel(responseToken, "#osu") + channelJoinEvent.joinChannel(responseToken, "#announce") + if userRank >= 3: + # Join admin chanenl if we are mod/admin + # TODO: Separate channels for mods and admins + channelJoinEvent.joinChannel(responseToken, "#admin") + + # Output channels info + for key, value in glob.channels.channels.items(): + if value.publicRead == True: + responseToken.enqueue(serverPackets.channelInfo(key)) + + responseToken.enqueue(serverPackets.friendList(userID)) + + # Send main menu icon and login notification if needed + if glob.banchoConf.config["menuIcon"] != "": + responseToken.enqueue(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) + + if glob.banchoConf.config["loginNotification"] != "": + responseToken.enqueue(serverPackets.notification(glob.banchoConf.config["loginNotification"])) + + # Get everyone else userpanel + # TODO: Better online users handling + for key, value in glob.tokens.tokens.items(): + responseToken.enqueue(serverPackets.userPanel(value.userID)) + responseToken.enqueue(serverPackets.userStats(value.userID)) + + # Send online users IDs array + responseToken.enqueue(serverPackets.onlineUsers()) + + # Get location and country from ip.zxq.co or database + if generalFunctions.stringToBool(glob.conf.config["server"]["localizeusers"]): + # Get location and country from IP + location = locationHelper.getLocation(requestIP) + country = countryHelper.getCountryID(locationHelper.getCountry(requestIP)) + else: + # Set location to 0,0 and get country from db + print("[!] Location skipped") + location = [0,0] + country = countryHelper.getCountryID(userHelper.getCountry(userID)) + + # Set location and country + responseToken.setLocation(location) + responseToken.setCountry(country) + + # Send to everyone our userpanel and userStats (so they now we have logged in) + glob.tokens.enqueueAll(serverPackets.userPanel(userID)) + glob.tokens.enqueueAll(serverPackets.userStats(userID)) + + # Set reponse data to right value and reset our queue + responseData = responseToken.queue + responseToken.resetQueue() + + # Print logged in message + consoleHelper.printColored("> {} logged in ({})".format(loginData[0], responseToken.token), bcolors.GREEN) + except exceptions.loginFailedException: + # Login failed error packet + # (we don't use enqueue because we don't have a token since login has failed) + err = True + responseData += serverPackets.loginFailed() + except exceptions.loginBannedException: + # Login banned error packet + err = True + responseData += serverPackets.loginBanned() + except exceptions.banchoMaintenanceException: + # Bancho is in maintenance mode + responseData += serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.") + responseData += serverPackets.loginError() + except exceptions.banchoRestartingException: + # Bancho is restarting + responseData += serverPackets.notification("Bancho is restarting. Try again in a few minutes.") + responseData += serverPackets.loginError() + finally: + # Print login failed message to console if needed + if err == True: + consoleHelper.printColored("> {}'s login failed".format(loginData[0]), bcolors.YELLOW) + + return (responseTokenString, responseData) diff --git a/logoutEvent.py b/logoutEvent.py new file mode 100644 index 0000000..fbbff40 --- /dev/null +++ b/logoutEvent.py @@ -0,0 +1,39 @@ +import glob +import consoleHelper +import bcolors +import serverPackets +import time + +def handle(userToken, _): + # get usertoken data + userID = userToken.userID + username = userToken.username + requestToken = userToken.token + + # Big client meme here. If someone logs out and logs in right after, + # the old logout packet will still be in the queue and will be sent to + # the server, so we accept logout packets sent at least 5 seconds after login + # if the user logs out before 5 seconds, he will be disconnected later with timeout check + if int(time.time()-userToken.loginTime) >= 5: + # Stop spectating if needed + if userToken.spectating != 0: + # The user was spectating someone + spectatorHostToken = glob.tokens.getTokenFromUserID(userToken.spectating) + if spectatorHostToken != None: + # The host is still online, send removeSpectator to him + spectatorHostToken.enqueue(serverPackets.removeSpectator(userID)) + + # Part all joined channels + for i in userToken.joinedChannels: + glob.channels.channels[i].userPart(userID) + + # TODO: Lobby left if joined + + # Enqueue our disconnection to everyone else + glob.tokens.enqueueAll(serverPackets.userLogout(userID)) + + # Delete token + glob.tokens.deleteToken(requestToken) + + # Console output + consoleHelper.printColored("> {} have been disconnected.".format(username), bcolors.YELLOW) diff --git a/match.py b/match.py new file mode 100644 index 0000000..a309ba0 --- /dev/null +++ b/match.py @@ -0,0 +1,656 @@ +# TODO: Enqueue all +import gameModes +import matchScoringTypes +import matchTeamTypes +import matchModModes +import slotStatuses +import glob +import consoleHelper +import bcolors +import serverPackets +import dataTypes +import matchTeams + +class match: + """Multiplayer match object""" + matchID = 0 + inProgress = False + mods = 0 + matchName = "" + matchPassword = "" + beatmapName = "" + beatmapID = 0 + beatmapMD5 = "" + slots = [] # list of dictionaries {"status": 0, "team": 0, "userID": -1, "mods": 0, "loaded": False, "skip": False, "complete": False} + hostUserID = 0 + gameMode = gameModes.std + matchScoringType = matchScoringTypes.score + matchTeamType = matchTeamTypes.headToHead + matchModMode = matchModModes.normal + seed = 0 + + def __init__(self, __matchID, __matchName, __matchPassword, __beatmapID, __beatmapName, __beatmapMD5, __gameMode, __hostUserID): + """ + Create a new match object + + __matchID -- match progressive identifier + __matchName -- match name, string + __matchPassword -- match md5 password. Leave empty for no password + __beatmapID -- beatmap ID + __beatmapName -- beatmap name, string + __beatmapMD5 -- beatmap md5 hash, string + __gameMode -- game mode ID. See gameModes.py + __hostUserID -- user id of the host + """ + self.matchID = __matchID + self.inProgress = False + self.mods = 0 + self.matchName = __matchName + self.matchPassword = __matchPassword + self.beatmapID = __beatmapID + self.beatmapName = __beatmapName + self.beatmapMD5 = __beatmapMD5 + self.hostUserID = __hostUserID + self.gameMode = __gameMode + self.matchScoringTypes = matchScoringTypes.score # default values + self.matchTeamType = matchTeamTypes.headToHead # default value + self.matchModMode = matchModModes.normal # default value + self.seed = 0 + + # Create all slots and reset them + self.slots = [] + for _ in range(0,16): + self.slots.append({"status": slotStatuses.free, "team": 0, "userID": -1, "mods": 0, "loaded": False, "skip": False, "complete": False}) + + + def getMatchData(self): + """ + Return binary match data structure for packetHelper + """ + # General match info + struct = [ + [self.matchID, dataTypes.uInt16], + [int(self.inProgress), dataTypes.byte], + [0, dataTypes.byte], + [self.mods, dataTypes.uInt32], + [self.matchName, dataTypes.string], + [self.matchPassword, dataTypes.string], + [self.beatmapName, dataTypes.string], + [self.beatmapID, dataTypes.uInt32], + [self.beatmapMD5, dataTypes.string], + ] + + # Slots status IDs, always 16 elements + for i in range(0,16): + struct.append([self.slots[i]["status"], dataTypes.byte]) + + # Slot teams, always 16 elements + for i in range(0,16): + struct.append([self.slots[i]["team"], dataTypes.byte]) + + # Slot user ID. Write only if slot is occupied + for i in range(0,16): + uid = self.slots[i]["userID"] + if uid > -1: + struct.append([uid, dataTypes.uInt32]) + + # Other match data + struct.extend([ + [self.hostUserID, dataTypes.sInt32], + [self.gameMode, dataTypes.byte], + [self.matchScoringType, dataTypes.byte], + [self.matchTeamType, dataTypes.byte], + [self.matchModMode, dataTypes.byte], + ]) + + # Slot mods if free mod is enabled + if self.matchModMode == matchModModes.freeMod: + for i in range(0,16): + struct.append([self.slots[i]["mods"], dataTypes.uInt32]) + + # Seed idk + struct.append([self.seed, dataTypes.uInt32]) + + return struct + + + + def setHost(self, newHost): + """ + Set room host to newHost and send him host packet + + newHost -- new host userID + """ + self.hostUserID = newHost + + # Send host packet to new host + token = glob.tokens.getTokenFromUserID(newHost) + if token != None: + token.enqueue(serverPackets.matchTransferHost()) + + consoleHelper.printColored("> MPROOM{}: {} is now the host".format(self.matchID, newHost), bcolors.BLUE) + + def setSlot(self, slotID, slotStatus = None, slotTeam = None, slotUserID = None, slotMods = None, slotLoaded = None, slotSkip = None, slotComplete = None): + """ + Set a slot to a specific userID and status + + slotID -- id of that slot (0-15) + slotStatus -- see slotStatuses.py + slotTeam -- team id + slotUserID -- user ID of user in that slot + slotMods -- mods enabled in that slot. 0 if not free mod. + slotLoaded -- loaded status True/False + slotSkip -- skip status True/False + slotComplete -- completed status True/False + + If Null is passed, that value won't be edited + """ + if slotStatus != None: + self.slots[slotID]["status"] = slotStatus + + if slotTeam != None: + self.slots[slotID]["team"] = slotTeam + + if slotUserID != None: + self.slots[slotID]["userID"] = slotUserID + + if slotMods != None: + self.slots[slotID]["mods"] = slotMods + + if slotLoaded != None: + self.slots[slotID]["loaded"] = slotLoaded + + if slotSkip != None: + self.slots[slotID]["skip"] = slotSkip + + if slotComplete != None: + self.slots[slotID]["complete"] = slotComplete + + + def setSlotMods(self, slotID, mods): + """ + Set slotID mods. Same as calling setSlot and then sendUpdate + + slotID -- slot number + mods -- new mods + """ + # Set new slot data and send update + self.setSlot(slotID, None, None, None, mods) + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Slot{} mods changed to {}".format(self.matchID, slotID, mods), bcolors.BLUE) + + + def toggleSlotReady(self, slotID): + """ + Switch slotID ready/not ready status + Same as calling setSlot and then sendUpdate + + slotID -- slot number + """ + # Update ready status and setnd update + oldStatus = self.slots[slotID]["status"] + if oldStatus == slotStatuses.ready: + newStatus = slotStatuses.notReady + else: + newStatus = slotStatuses.ready + self.setSlot(slotID, newStatus, None, None, None) + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Slot{} changed ready status to {}".format(self.matchID, slotID, self.slots[slotID]["status"]), bcolors.BLUE) + + def toggleSlotLock(self, slotID): + """ + Lock a slot + Same as calling setSlot and then sendUpdate + + slotID -- slot number + """ + # Get token of user in that slot (if there's someone) + if self.slots[slotID]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[slotID]["userID"]) + else: + token = None + + # Check if slot is already locked + if self.slots[slotID]["status"] == slotStatuses.locked: + newStatus = slotStatuses.free + else: + newStatus = slotStatuses.locked + + # Set new slot status + self.setSlot(slotID, newStatus, 0, -1, 0) + if token != None: + # Send updated settings to kicked user, so he returns to lobby + token.enqueue(serverPackets.updateMatch(self.matchID)) + + # Send updates to everyone else + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Slot{} {}".format(self.matchID, slotID, "locked" if newStatus == slotStatuses.locked else "unlocked"), bcolors.BLUE) + + def playerLoaded(self, userID): + """ + Set a player loaded status to True + + userID -- ID of user + """ + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set loaded to True + self.slots[slotID]["loaded"] = True + consoleHelper.printColored("> MPROOM{}: User {} loaded".format(self.matchID, userID), bcolors.BLUE) + + # Check all loaded + total = 0 + loaded = 0 + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.playing: + total+=1 + if self.slots[i]["loaded"] == True: + loaded+=1 + + if total == loaded: + self.allPlayersLoaded() + + + def allPlayersLoaded(self): + """Send allPlayersLoaded packet to every playing usr in match""" + for i in range(0,16): + if self.slots[i]["userID"] > -1 and self.slots[i]["status"] == slotStatuses.playing: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.allPlayersLoaded()) + + consoleHelper.printColored("> MPROOM{}: All players loaded! Corrispondere iniziare in 3...".format(self.matchID), bcolors.BLUE) + + + def playerSkip(self, userID): + """ + Set a player skip status to True + + userID -- ID of user + """ + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set skip to True + self.slots[slotID]["skip"] = True + consoleHelper.printColored("> MPROOM{}: User {} skipped".format(self.matchID, userID), bcolors.BLUE) + + # Send skip packet to every playing useR + for i in range(0,16): + uid = self.slots[i]["userID"] + if self.slots[i]["status"] == slotStatuses.playing and uid > -1: + token = glob.tokens.getTokenFromUserID(uid) + if token != None: + print("Enqueueueue {}".format(uid)) + token.enqueue(serverPackets.playerSkipped(uid)) + + # Check all skipped + total = 0 + skipped = 0 + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.playing: + total+=1 + if self.slots[i]["skip"] == True: + skipped+=1 + + if total == skipped: + self.allPlayersSkipped() + + def allPlayersSkipped(self): + """Send allPlayersSkipped packet to every playing usr in match""" + for i in range(0,16): + if self.slots[i]["userID"] > -1 and self.slots[i]["status"] == slotStatuses.playing: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.allPlayersSkipped()) + + consoleHelper.printColored("> MPROOM{}: All players skipped!".format(self.matchID), bcolors.BLUE) + + def playerCompleted(self, userID): + """ + Set userID's slot completed to True + + userID -- ID of user + """ + slotID = self.getUserSlotID(userID) + if slotID == None: + return + self.setSlot(slotID, None, None, None, None, None, None, True) + + # Console output + consoleHelper.printColored("> MPROOM{}: User {} has completed".format(self.matchID, userID), bcolors.BLUE) + + # Check all completed + total = 0 + completed = 0 + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.playing: + total+=1 + if self.slots[i]["complete"] == True: + completed+=1 + + if total == completed: + self.allPlayersCompleted() + + def allPlayersCompleted(self): + """Cleanup match stuff and send match end packet to everyone""" + + # Reset inProgress + self.inProgress = False + + # Reset slots + for i in range(0,16): + if self.slots[i]["userID"] > -1 and self.slots[i]["status"] == slotStatuses.playing: + self.slots[i]["status"] = slotStatuses.notReady + self.slots[i]["loaded"] = False + self.slots[i]["skip"] = False + self.slots[i]["complete"] = False + + # Send match update + self.sendUpdate() + + # Send match complete + for i in range(0,16): + if self.slots[i]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.matchComplete()) + + # Console output + consoleHelper.printColored("> MPROOM{}: Match completed".format(self.matchID), bcolors.BLUE) + + + + def getUserSlotID(self, userID): + """ + Get slot ID occupied by userID + + return -- slot id if found, None if user is not in room + """ + + for i in range(0,16): + if self.slots[i]["userID"] == userID: + return i + + return None + + def userJoin(self, userID): + """ + Add someone to users in match + + userID -- user id of the user + return -- True if join success, False if fail (room is full) + """ + + # Find first free slot + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.free: + # Occupy slot + self.setSlot(i, slotStatuses.notReady, 0, userID, 0) + + # Send updated match data + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: {} joined the room".format(self.matchID, userID), bcolors.BLUE) + + return True + + return False + + def userLeft(self, userID): + """ + Remove someone from users in match + + userID -- user if of the user + """ + + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set that slot to free + self.setSlot(slotID, slotStatuses.free, 0, -1, 0) + + # Check if everyone left + if self.countUsers() == 0: + # Dispose match + glob.matches.disposeMatch(self.matchID) + consoleHelper.printColored("> MPROOM{}: Room disposed".format(self.matchID), bcolors.BLUE) + return + + # Check if host left + if userID == self.hostUserID: + # Give host to someone else + for i in range(0,16): + uid = self.slots[i]["userID"] + if uid > -1: + self.setHost(uid) + break + + # Send updated match data + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: {} left the room".format(self.matchID, userID), bcolors.BLUE) + + + def userChangeSlot(self, userID, newSlotID): + """ + Change userID slot to newSlotID + + userID -- user that changed slot + newSlotID -- slot id of new slot + """ + + # Make sure the user is in room + oldSlotID = self.getUserSlotID(userID) + if oldSlotID == None: + return + + # Make sure there is no one inside new slot + if self.slots[newSlotID]["userID"] > -1: + return + + # Get old slot data + oldData = self.slots[oldSlotID].copy() + + # Free old slot + self.setSlot(oldSlotID, slotStatuses.free, 0, -1, 0) + + # Occupy new slot + self.setSlot(newSlotID, oldData["status"], oldData["team"], userID, oldData["mods"]) + + # Send updated match data + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: {} moved to slot {}".format(self.matchID, userID, newSlotID), bcolors.BLUE) + + def changePassword(self, newPassword): + """ + Change match password to newPassword + + newPassword -- new password string + """ + self.matchPassword = newPassword + + # Send password change to every user in match + for i in range(0,16): + if self.slots[i]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.changeMatchPassword(self.matchPassword)) + + # Send new match settings too + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: Password changed to {}".format(self.matchID, self.matchPassword), bcolors.BLUE) + + + def changeMatchMods(self, mods): + """ + Set match global mods + + mods -- mods bitwise int thing + """ + # Set new mods and send update + self.mods = mods + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Mods changed to {}".format(self.matchID, self.mods), bcolors.BLUE) + + def userHasBeatmap(self, userID, has = True): + """ + Set no beatmap status for userID + + userID -- ID of user + has -- True if has beatmap, false if not + """ + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set slot + self.setSlot(slotID, slotStatuses.noMap if not has else slotStatuses.notReady) + + # Send updates + self.sendUpdate() + + def transferHost(self, slotID): + """ + Transfer host to slotID + + slotID -- ID of slot + """ + # Make sure there is someone in that slot + uid = self.slots[slotID]["userID"] + if uid == -1: + return + + # Transfer host + self.setHost(uid) + + # Send updates + self.sendUpdate() + + + def playerFailed(self, userID): + """ + Send userID's failed packet to everyone in match + + userID -- ID of user + """ + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Send packet to everyone + for i in range(0,16): + uid = self.slots[i]["userID"] + if uid > -1: + token = glob.tokens.getTokenFromUserID(uid) + if token != None: + token.enqueue(serverPackets.playerFailed(slotID)) + + # Console output + consoleHelper.printColored("> MPROOM{}: {} has failed!".format(self.matchID, userID), bcolors.BLUE) + + + def invite(self, fro, to): + """ + Fro invites to in this match. + + fro -- sender userID + to -- receiver userID + """ + + # Get tokens + froToken = glob.tokens.getTokenFromUserID(fro) + toToken = glob.tokens.getTokenFromUserID(to) + if froToken == None or toToken == None: + return + + # FokaBot is too busy + if to == 999: + froToken.enqueue(serverPackets.sendMessage("FokaBot", froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.")) + + # Send message + message = "Come join my multiplayer match: \"[osump://{}/{} {}]\"".format(self.matchID, self.matchPassword.replace(" ", "_"), self.matchName) + toToken.enqueue(serverPackets.sendMessage(froToken.username, toToken.username, message)) + + + def countUsers(self): + """ + Return how many players are in that match + + return -- number of users + """ + + c = 0 + for i in range(0,16): + if self.slots[i]["userID"] > -1: + c+=1 + + return c + + def changeTeam(self, userID): + """ + Change userID's team + + userID -- id of user + """ + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Update slot and send update + newTeam = matchTeams.blue if self.slots[slotID]["team"] == matchTeams.red else matchTeams.red + self.setSlot(slotID, None, newTeam) + self.sendUpdate() + + + + def sendUpdate(self): + # Send to users in room + for i in range(0,16): + if self.slots[i]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.updateMatch(self.matchID)) + + # Send to users in lobby + for i in glob.matches.usersInLobby: + token = glob.tokens.getTokenFromUserID(i) + if token != None: + token.enqueue(serverPackets.updateMatch(self.matchID)) + + def checkTeams(self): + """ + Check if match teams are valid + + return -- True if valid, False if invalid + """ + if match.matchTeamType != matchTeamTypes.teamVs or matchTeamTypes != matchTeamTypes.tagTeamVs: + # Teams are always valid if we have no teams + return True + + # We have teams, check if they are valid + firstTeam = -1 + for i in range(0,16): + if self.slots[i]["userID"] > -1 and (self.slots[i]["status"]&slotStatuses.noMap) == 0: + if firstTeam == -1: + firstTeam = self.slots[i]["team"] + elif firstTeam != self.slots[i]["teams"]: + consoleHelper.printColored("> MPROOM{}: Teams are valid".format(self.matchID), bcolors.BLUE) + return True + + consoleHelper.printColored("> MPROOM{}: Invalid teams!".format(self.matchID), bcolors.RED) + return False diff --git a/matchBeatmapEvent.py b/matchBeatmapEvent.py new file mode 100644 index 0000000..06ddd0b --- /dev/null +++ b/matchBeatmapEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData, has): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set has beatmap/no beatmap + match.userHasBeatmap(userID, has) diff --git a/matchChangeTeamEvent.py b/matchChangeTeamEvent.py new file mode 100644 index 0000000..915c30e --- /dev/null +++ b/matchChangeTeamEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, _): + # Read token data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Get match object + match = glob.matches.matches[matchID] + + # Change team + match.changeTeam(userID) diff --git a/matchCompleteEvent.py b/matchCompleteEvent.py new file mode 100644 index 0000000..0a45e18 --- /dev/null +++ b/matchCompleteEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set our match complete + match.playerCompleted(userID) diff --git a/matchFailedEvent.py b/matchFailedEvent.py new file mode 100644 index 0000000..b040ad7 --- /dev/null +++ b/matchFailedEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, _): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Match exists, get object + match = glob.matches.matches[matchID] + + # Fail user + match.playerFailed(userID) diff --git a/matchFramesEvent.py b/matchFramesEvent.py new file mode 100644 index 0000000..50a9ca1 --- /dev/null +++ b/matchFramesEvent.py @@ -0,0 +1,35 @@ +import glob +import slotStatuses +import serverPackets + +def handle(userToken, packetData): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Change slot id in packetData + slotID = match.getUserSlotID(userID) + '''opd = packetData[4] + packetData = bytearray(packetData) + packetData[4] = slotID + print("User: {}, slot {}, oldPackData: {}, packData {}".format(userID, slotID, opd, packetData[4]))''' + + # Enqueue frames to who's playing + for i in range(0,16): + if match.slots[i]["userID"] > -1 and match.slots[i]["status"] == slotStatuses.playing: + token = glob.tokens.getTokenFromUserID(match.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.matchFrames(slotID, packetData)) diff --git a/matchHasBeatmapEvent.py b/matchHasBeatmapEvent.py new file mode 100644 index 0000000..118afb2 --- /dev/null +++ b/matchHasBeatmapEvent.py @@ -0,0 +1,3 @@ +import matchBeatmapEvent +def handle(userToken, packetData): + matchBeatmapEvent.handle(userToken, packetData, True) diff --git a/matchInviteEvent.py b/matchInviteEvent.py new file mode 100644 index 0000000..f27ccaa --- /dev/null +++ b/matchInviteEvent.py @@ -0,0 +1,24 @@ +import clientPackets +import glob + +def handle(userToken, packetData): + # Read token and packet data + userID = userToken.userID + packetData = clientPackets.matchInvite(packetData) + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Get match object + match = glob.matches.matches[matchID] + + # Send invite + match.invite(userID, packetData["userID"]) diff --git a/matchList.py b/matchList.py new file mode 100644 index 0000000..c78ac4f --- /dev/null +++ b/matchList.py @@ -0,0 +1,80 @@ +import match +import glob +import serverPackets + +class matchList: + matches = {} + usersInLobby = [] + lastID = 1 + + def __init__(self): + """Initialize a matchList object""" + self.matches = {} + self.usersInLobby = [] + self.lastID = 1 + + def createMatch(self, __matchName, __matchPassword, __beatmapID, __beatmapName, __beatmapMD5, __gameMode, __hostUserID): + """ + Add a new match to matches list + + __matchName -- match name, string + __matchPassword -- match md5 password. Leave empty for no password + __beatmapID -- beatmap ID + __beatmapName -- beatmap name, string + __beatmapMD5 -- beatmap md5 hash, string + __gameMode -- game mode ID. See gameModes.py + __hostUserID -- user id of who created the match + return -- match ID + """ + # Add a new match to matches list + matchID = self.lastID + self.lastID+=1 + self.matches[matchID] = match.match(matchID, __matchName, __matchPassword, __beatmapID, __beatmapName, __beatmapMD5, __gameMode, __hostUserID) + return matchID + + + def lobbyUserJoin(self, __userID): + """ + Add userID to users in lobby + + __userID -- user who joined mp lobby + """ + + # Make sure the user is not already in mp lobby + if __userID not in self.usersInLobby: + # We don't need to join #lobby, client will automatically send a packet for it + self.usersInLobby.append(__userID) + + + def lobbyUserPart(self, __userID): + """ + Remove userID from users in lobby + + __userID -- user who left mp lobby + """ + + # Make sure the user is in mp lobby + if __userID in self.usersInLobby: + # Part lobby and #lobby channel + self.usersInLobby.remove(__userID) + + + def disposeMatch(self, __matchID): + """ + Destroy match object with id = __matchID + + __matchID -- ID of match to dispose + """ + + # Make sure the match exists + if __matchID not in self.matches: + return + + # Remove match object + self.matches.pop(__matchID) + + # Send match dispose packet to everyone in lobby + for i in self.usersInLobby: + token = glob.tokens.getTokenFromUserID(i) + if token != None: + token.enqueue(serverPackets.disposeMatch(__matchID)) diff --git a/matchLockEvent.py b/matchLockEvent.py new file mode 100644 index 0000000..4041b2e --- /dev/null +++ b/matchLockEvent.py @@ -0,0 +1,23 @@ +import glob +import clientPackets + +def handle(userToken, packetData): + # Get token data + userID = userToken.userID + + # Get packet data + packetData = clientPackets.lockSlot(packetData) + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + match = glob.matches.matches[matchID] + + # Make sure we aren't locking our slot + ourSlot = match.getUserSlotID(userID) + if packetData["slotID"] == ourSlot: + return + + # Lock/Unlock slot + match.toggleSlotLock(packetData["slotID"]) diff --git a/matchModModes.py b/matchModModes.py new file mode 100644 index 0000000..0b8ea87 --- /dev/null +++ b/matchModModes.py @@ -0,0 +1,2 @@ +normal = 0 +freeMod = 1 diff --git a/matchNoBeatmapEvent.py b/matchNoBeatmapEvent.py new file mode 100644 index 0000000..efbff0d --- /dev/null +++ b/matchNoBeatmapEvent.py @@ -0,0 +1,3 @@ +import matchBeatmapEvent +def handle(userToken, packetData): + matchBeatmapEvent.handle(userToken, packetData, False) diff --git a/matchPlayerLoadEvent.py b/matchPlayerLoadEvent.py new file mode 100644 index 0000000..449a56b --- /dev/null +++ b/matchPlayerLoadEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData): + # Get userToken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set our load status + match.playerLoaded(userID) diff --git a/matchReadyEvent.py b/matchReadyEvent.py new file mode 100644 index 0000000..7ac992a --- /dev/null +++ b/matchReadyEvent.py @@ -0,0 +1,16 @@ +import glob + +def handle(userToken, _): + # Get usertoken data + userID = userToken.userID + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + match = glob.matches.matches[matchID] + + # Get our slotID and change ready status + slotID = match.getUserSlotID(userID) + if slotID != None: + match.toggleSlotReady(slotID) diff --git a/matchScoringTypes.py b/matchScoringTypes.py new file mode 100644 index 0000000..888f851 --- /dev/null +++ b/matchScoringTypes.py @@ -0,0 +1,3 @@ +score = 0 +accuracy = 1 +combo = 2 diff --git a/matchSkipEvent.py b/matchSkipEvent.py new file mode 100644 index 0000000..c06f114 --- /dev/null +++ b/matchSkipEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData): + # Get userToken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Skip + match.playerSkip(userID) diff --git a/matchStartEvent.py b/matchStartEvent.py new file mode 100644 index 0000000..b294229 --- /dev/null +++ b/matchStartEvent.py @@ -0,0 +1,47 @@ +import glob +import slotStatuses +import serverPackets + +def handle(userToken, _): + # TODO: Host check + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + force = False # TODO: Force thing + + # Make sure we have enough players + if (match.countUsers() < 2 or not match.checkTeams()) and not force: + return + + # Change inProgress value + match.inProgress = True + + # Set playing to ready players and set load, skip and complete to False + for i in range(0,16): + if (match.slots[i]["status"] & slotStatuses.ready) > 0: + match.slots[i]["status"] = slotStatuses.playing + match.slots[i]["loaded"] = False + match.slots[i]["skip"] = False + match.slots[i]["complete"] = False + + # Send match start packet + for i in range(0,16): + if (match.slots[i]["status"] & slotStatuses.playing) > 0 and match.slots[i]["userID"] != -1: + token = glob.tokens.getTokenFromUserID(match.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.matchStart(matchID)) + + # Send updates + match.sendUpdate() diff --git a/matchTeamTypes.py b/matchTeamTypes.py new file mode 100644 index 0000000..07d43bd --- /dev/null +++ b/matchTeamTypes.py @@ -0,0 +1,4 @@ +headToHead = 0 +tagCoop = 1 +teamVs = 2 +tagTeamVs = 3 diff --git a/matchTeams.py b/matchTeams.py new file mode 100644 index 0000000..ef47898 --- /dev/null +++ b/matchTeams.py @@ -0,0 +1,3 @@ +noTeam = 0 +blue = 1 +red = 2 diff --git a/matchTransferHostEvent.py b/matchTransferHostEvent.py new file mode 100644 index 0000000..4dff540 --- /dev/null +++ b/matchTransferHostEvent.py @@ -0,0 +1,23 @@ +import glob +import clientPackets + +def handle(userToken, packetData): + # Get packet data + packetData = clientPackets.transferHost(packetData) + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Match exists, get object + match = glob.matches.matches[matchID] + + # Transfer host + match.transferHost(packetData["slotID"]) diff --git a/mods.py b/mods.py new file mode 100644 index 0000000..ee305dd --- /dev/null +++ b/mods.py @@ -0,0 +1,30 @@ +Nomod = 0 +NoFail = 1 +Easy = 2 +NoVideo = 4 +Hidden = 8 +HardRock = 16 +SuddenDeath = 32 +DoubleTime = 64 +Relax = 128 +HalfTime = 256 +Nightcore = 512 +Flashlight = 1024 +Autoplay = 2048 +SpunOut = 4096 +Relax2 = 8192 +Perfect = 16384 +Key4 = 32768 +Key5 = 65536 +Key6 = 131072 +Key7 = 262144 +Key8 = 524288 +keyMod = 1015808 +FadeIn = 1048576 +Random = 2097152 +LastMod = 4194304 +Key9 = 16777216 +Key10 = 33554432 +Key1 = 67108864 +Key3 = 134217728 +Key2 = 268435456 diff --git a/osuToken.py b/osuToken.py new file mode 100644 index 0000000..7ac5493 --- /dev/null +++ b/osuToken.py @@ -0,0 +1,227 @@ +import uuid +import actions +import gameModes +import userHelper +import time +import consoleHelper +import bcolors +import serverPackets +import logoutEvent + +class token: + """Osu Token object + + token -- token string + userID -- userID associated to that token + username -- username relative to userID (cache) + rank -- rank (permissions) relative to userID (cache) + actionID -- current user action (see actions.py) + actionText -- current user action text + actionMd5 -- md5 relative to user action + actionMods -- current acton mods + gameMode -- current user game mode + location -- [latitude,longitude] + queue -- packets queue + joinedChannels -- list. Contains joined channel names + spectating -- userID of spectating user. 0 if not spectating. + spectators -- list. Contains userIDs of spectators + country -- osu country code. Use countryHelper to convert from letter country code to osu country code + pingTime -- latest packet received UNIX time + loginTime -- login UNIX time + """ + + token = "" + userID = 0 + username = "" + rank = 0 + actionID = actions.idle + actionText = "" + actionMd5 = "" + actionMods = 0 + gameMode = gameModes.std + + country = 0 + location = [0,0] + + queue = bytes() + joinedChannels = [] + + spectating = 0 + spectators = [] + + pingTime = 0 + loginTime = 0 + + awayMessage = "" + + matchID = -1 + + + def __init__(self, __userID, __token = None): + """ + Create a token object and set userID and token + + __userID -- user associated to this token + __token -- if passed, set token to that value + if not passed, token will be generated + """ + + # Set stuff + self.userID = __userID + self.username = userHelper.getUsername(self.userID) + self.rank = userHelper.getRankPrivileges(self.userID) + self.loginTime = int(time.time()) + self.pingTime = self.loginTime + + # Default variables + self.spectators = [] + self.spectating = 0 + self.location = [0,0] + self.joinedChannels = [] + self.actionID = actions.idle + self.actionText = "" + self.actionMods = 0 + self.gameMode = gameModes.std + self.awayMessage = "" + self.matchID = -1 + + # Generate/set token + if __token != None: + self.token = __token + else: + self.token = str(uuid.uuid4()) + + + def enqueue(self, __bytes): + """ + Add bytes (packets) to queue + + __bytes -- (packet) bytes to enqueue + """ + + self.queue += __bytes + + + def resetQueue(self): + """Resets the queue. Call when enqueued packets have been sent""" + self.queue = bytes() + + + def joinChannel(self, __channel): + """Add __channel to joined channels list + + __channel -- channel name""" + + if __channel not in self.joinedChannels: + self.joinedChannels.append(__channel) + + + def partChannel(self, __channel): + """Remove __channel from joined channels list + + __channel -- channel name""" + + if __channel in self.joinedChannels: + self.joinedChannels.remove(__channel) + + + def setLocation(self, __location): + """Set location (latitude and longitude) + + __location -- [latitude, longitude]""" + + self.location = __location + + + def getLatitude(self): + """Get latitude + + return -- latitude""" + + return self.location[0] + + + def getLongitude(self): + """Get longitude + + return -- longitude""" + return self.location[1] + + + def startSpectating(self, __userID): + """Set the spectating user to __userID + + __userID -- target userID""" + self.spectating = __userID + + + def stopSpectating(self): + """Set the spectating user to 0, aka no user""" + self.spectating = 0 + + + def addSpectator(self, __userID): + """Add __userID to our spectators + + userID -- new spectator userID""" + + # Add userID to spectators if not already in + if __userID not in self.spectators: + self.spectators.append(__userID) + + + def removeSpectator(self, __userID): + """Remove __userID from our spectators + + userID -- old spectator userID""" + + # Remove spectator + if __userID in self.spectators: + self.spectators.remove(__userID) + + + def setCountry(self, __countryID): + """Set country to __countryID + + __countryID -- numeric country ID. See countryHelper.py""" + + self.country = __countryID + + + def getCountry(self): + """Get numeric country ID + + return -- numeric country ID. See countryHelper.py""" + + return self.country + + + def updatePingTime(self): + """Update latest ping time""" + self.pingTime = int(time.time()) + + def setAwayMessage(self, __awayMessage): + """Set a new away message""" + self.awayMessage = __awayMessage + + def joinMatch(self, __matchID): + """ + Set match to matchID + + __matchID -- new match ID + """ + self.matchID = __matchID + + def partMatch(self): + """Set match to -1""" + self.matchID = -1 + + def kick(self): + """Kick this user from the server""" + # Send packet to target + consoleHelper.printColored("> {} has been disconnected. (kick)".format(self.username), bcolors.YELLOW) + self.enqueue(serverPackets.notification("You have been kicked from the server. Please login again.")) + self.enqueue(serverPackets.loginFailed()) + + # Logout event + logoutEvent.handle(self, None) diff --git a/packetHelper.py b/packetHelper.py new file mode 100644 index 0000000..c86e8f3 --- /dev/null +++ b/packetHelper.py @@ -0,0 +1,249 @@ +import struct +import dataTypes + +def uleb128Encode(num): + """ + Encode int -> uleb128 + + num -- int to encode + return -- bytearray with encoded number + """ + + arr = bytearray() + length = 0 + + if num == 0: + return bytearray(b"\x00") + + while num > 0: + arr.append(num & 127) + num = num >> 7 + if num != 0: + arr[length] = arr[length] | 128 + length+=1 + + return arr + + +def uleb128Decode(num): + """ + Decode uleb128 -> int + + num -- encoded uleb128 + return -- list. [total, length] + """ + + shift = 0 + + arr = [0,0] #total, length + + while True: + b = num[arr[1]] + arr[1]+=1 + arr[0] = arr[0] | (int(b & 127) << shift) + if b & 128 == 0: + break + shift += 7 + + return arr + + +def unpackData(__data, __dataType): + """ + Unpacks data according to dataType + + __data -- bytes array to unpack + __dataType -- data type. See dataTypes.py + + return -- unpacked bytes + """ + + # Get right pack Type + if __dataType == dataTypes.uInt16: + unpackType = " {} has left multiplayer lobby".format(username), bcolors.BLUE) diff --git a/partMatchEvent.py b/partMatchEvent.py new file mode 100644 index 0000000..b8b3122 --- /dev/null +++ b/partMatchEvent.py @@ -0,0 +1,27 @@ +import glob +import serverPackets + +def handle(userToken, _): + # get data from usertoken + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set slot to free + match.userLeft(userID) + + # Set usertoken match to -1 + userToken.partMatch() + userToken.enqueue(serverPackets.channelKicked("#multiplayer")) diff --git a/passwordHelper.py b/passwordHelper.py new file mode 100644 index 0000000..8523899 --- /dev/null +++ b/passwordHelper.py @@ -0,0 +1,36 @@ +import crypt +import base64 +import bcrypt + +def checkOldPassword(password, salt, rightPassword): + """ + Check if password+salt corresponds to rightPassword + + password -- input password + salt -- password's salt + rightPassword -- right password + return -- bool + """ + + return (rightPassword == crypt.crypt(password, "$2y$"+str(base64.b64decode(salt)))) + +def checkNewPassword(password, dbPassword): + """ + Check if a password (version 2) is right. + + password -- input password + dbPassword -- the password in the database + return -- bool + """ + password = password.encode("utf8") + dbPassword = dbPassword.encode("utf8") + return bcrypt.hashpw(password, dbPassword) == dbPassword + +def genBcrypt(password): + """ + Bcrypts a password. + + password -- the password to hash. + return -- bytestring + """ + return bcrypt.hashpw(password.encode("utf8"), bcrypt.gensalt(10, b'2a')) diff --git a/pep.py b/pep.py new file mode 100644 index 0000000..5c041cc --- /dev/null +++ b/pep.py @@ -0,0 +1,352 @@ +"""Hello, pep.py here, ex-owner of ripple and prime minister of Ripwot.""" +import logging +import sys +import flask +import datetime + +# Tornado server +from tornado.wsgi import WSGIContainer +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop + +# pep.py files +import bcolors +import packetIDs +import serverPackets +import config +import exceptions +import glob +import fokabot +import banchoConfig + +import sendPublicMessageEvent +import sendPrivateMessageEvent +import channelJoinEvent +import channelPartEvent +import changeActionEvent +import cantSpectateEvent +import startSpectatingEvent +import stopSpectatingEvent +import spectateFramesEvent +import friendAddEvent +import friendRemoveEvent +import logoutEvent +import loginEvent +import setAwayMessageEvent +import joinLobbyEvent +import createMatchEvent +import partLobbyEvent +import changeSlotEvent +import joinMatchEvent +import partMatchEvent +import changeMatchSettingsEvent +import changeMatchPasswordEvent +import changeMatchModsEvent +import matchReadyEvent +import matchLockEvent +import matchStartEvent +import matchPlayerLoadEvent +import matchSkipEvent +import matchFramesEvent +import matchCompleteEvent +import matchNoBeatmapEvent +import matchHasBeatmapEvent +import matchTransferHostEvent +import matchFailedEvent +import matchInviteEvent +import matchChangeTeamEvent + +# pep.py helpers +import packetHelper +import consoleHelper +import databaseHelper +import responseHelper +import generalFunctions +import systemHelper + +# Create flask instance +app = flask.Flask(__name__) + +# Get flask logger +flaskLogger = logging.getLogger("werkzeug") + +# Ci trigger +@app.route("/ci-trigger") +@app.route("/api/ci-trigger") +def ciTrigger(): + # Ci restart trigger + + # Get ket from GET + key = flask.request.args.get('k') + + # Get request ip + requestIP = flask.request.headers.get('X-Real-IP') + if requestIP == None: + requestIP = flask.request.remote_addr + + # Check key + if key is None or key != glob.conf.config["ci"]["key"]: + consoleHelper.printColored("[!] Invalid ci trigger from {}".format(requestIP), bcolors.RED) + return flask.jsonify({"response" : "-1"}) + + # Ci event triggered, schedule server shutdown + consoleHelper.printColored("[!] Ci event triggered from {}".format(requestIP), bcolors.PINK) + systemHelper.scheduleShutdown(5, False, "A new Bancho update is available and the server will be restarted in 5 seconds. Thank you for your patience.") + + return flask.jsonify({"response" : 1}) + + +@app.route("/api/server-status") +def serverStatus(): + # Server status api + # 1: Online + # -1: Restarting + return flask.jsonify({ + "response" : 200, + "status" : -1 if glob.restarting == True else 1 + }) + + +# Main bancho server +@app.route("/", methods=['GET', 'POST']) +def banchoServer(): + if flask.request.method == 'POST': + + # Track time if needed + if serverOutputRequestTime == True: + # Start time + st = datetime.datetime.now() + + # Client's token string and request data + requestTokenString = flask.request.headers.get('osu-token') + requestData = flask.request.data + + # Server's token string and request data + responseTokenString = "ayy" + responseData = bytes() + + if requestTokenString == None: + # No token, first request. Handle login. + responseTokenString, responseData = loginEvent.handle(flask.request) + else: + try: + # This is not the first packet, send response based on client's request + # Packet start position, used to read stacked packets + pos = 0 + + # Make sure the token exists + if requestTokenString not in glob.tokens.tokens: + raise exceptions.tokenNotFoundException() + + # Token exists, get its object + userToken = glob.tokens.tokens[requestTokenString] + + # Keep reading packets until everything has been read + while pos < len(requestData): + # Get packet from stack starting from new packet + leftData = requestData[pos:] + + # Get packet ID, data length and data + packetID = packetHelper.readPacketID(leftData) + dataLength = packetHelper.readPacketLength(leftData) + packetData = requestData[pos:(pos+dataLength+7)] + + # Console output if needed + if serverOutputPackets == True and packetID != 4: + consoleHelper.printColored("Incoming packet ({})({}):".format(requestTokenString, userToken.username), bcolors.GREEN) + consoleHelper.printColored("Packet code: {}\nPacket length: {}\nSingle packet data: {}\n".format(str(packetID), str(dataLength), str(packetData)), bcolors.YELLOW) + + # Event handler + def handleEvent(ev): + def wrapper(): + ev.handle(userToken, packetData) + return wrapper + + eventHandler = { + # TODO: Rename packets and events + # TODO: Host check for multi + packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent), + packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent), + packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent), + packetIDs.client_channelJoin: handleEvent(channelJoinEvent), + packetIDs.client_channelPart: handleEvent(channelPartEvent), + packetIDs.client_changeAction: handleEvent(changeActionEvent), + packetIDs.client_startSpectating: handleEvent(startSpectatingEvent), + packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent), + packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent), + packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent), + packetIDs.client_friendAdd: handleEvent(friendAddEvent), + packetIDs.client_friendRemove: handleEvent(friendRemoveEvent), + packetIDs.client_logout: handleEvent(logoutEvent), + packetIDs.client_joinLobby: handleEvent(joinLobbyEvent), + packetIDs.client_partLobby: handleEvent(partLobbyEvent), + packetIDs.client_createMatch: handleEvent(createMatchEvent), + packetIDs.client_joinMatch: handleEvent(joinMatchEvent), + packetIDs.client_partMatch: handleEvent(partMatchEvent), + packetIDs.client_matchChangeSlot: handleEvent(changeSlotEvent), + packetIDs.client_matchChangeSettings: handleEvent(changeMatchSettingsEvent), + packetIDs.client_matchChangePassword: handleEvent(changeMatchPasswordEvent), + packetIDs.client_matchChangeMods: handleEvent(changeMatchModsEvent), + packetIDs.client_matchReady: handleEvent(matchReadyEvent), + packetIDs.client_matchNotReady: handleEvent(matchReadyEvent), + packetIDs.client_matchLock: handleEvent(matchLockEvent), + packetIDs.client_matchStart: handleEvent(matchStartEvent), + packetIDs.client_matchLoadComplete: handleEvent(matchPlayerLoadEvent), + packetIDs.client_matchSkipRequest: handleEvent(matchSkipEvent), + packetIDs.client_matchScoreUpdate: handleEvent(matchFramesEvent), + packetIDs.client_matchComplete: handleEvent(matchCompleteEvent), + packetIDs.client_matchNoBeatmap: handleEvent(matchNoBeatmapEvent), + packetIDs.client_matchHasBeatmap: handleEvent(matchHasBeatmapEvent), + packetIDs.client_matchTransferHost: handleEvent(matchTransferHostEvent), + packetIDs.client_matchFailed: handleEvent(matchFailedEvent), + packetIDs.client_invite: handleEvent(matchInviteEvent), + packetIDs.client_matchChangeTeam: handleEvent(matchChangeTeamEvent) + } + + if packetID != 4: + if packetID in eventHandler: + eventHandler[packetID]() + else: + consoleHelper.printColored("[!] Unknown packet id from {} ({})".format(requestTokenString, packetID), bcolors.RED) + + # Update pos so we can read the next stacked packet + # +7 because we add packet ID bytes, unused byte and data length bytes + pos += dataLength+7 + + # Token queue built, send it + responseTokenString = userToken.token + responseData = userToken.queue + userToken.resetQueue() + + # Update ping time for timeout + userToken.updatePingTime() + except exceptions.tokenNotFoundException: + # Token not found. Disconnect that user + responseData = serverPackets.loginError() + responseData += serverPackets.notification("Whoops! Something went wrong, please login again.") + consoleHelper.printColored("[!] Received packet from unknown token ({}).".format(requestTokenString), bcolors.RED) + consoleHelper.printColored("> {} have been disconnected (invalid token)".format(requestTokenString), bcolors.YELLOW) + + if serverOutputRequestTime == True: + # End time + et = datetime.datetime.now() + + # Total time: + tt = float((et.microsecond-st.microsecond)/1000) + consoleHelper.printColored("Request time: {}ms".format(tt), bcolors.PINK) + + # Send server's response to client + # We don't use token object because we might not have a token (failed login) + return responseHelper.generateResponse(responseTokenString, responseData) + else: + # Not a POST request, send html page + return responseHelper.HTMLResponse() + + +if __name__ == "__main__": + # Server start + consoleHelper.printServerStartHeader(True) + + # Read config.ini + consoleHelper.printNoNl("> Loading config file... ") + glob.conf = config.config("config.ini") + + if glob.conf.default == True: + # We have generated a default config.ini, quit server + consoleHelper.printWarning() + consoleHelper.printColored("[!] config.ini not found. A default one has been generated.", bcolors.YELLOW) + consoleHelper.printColored("[!] Please edit your config.ini and run the server again.", bcolors.YELLOW) + sys.exit() + + # If we haven't generated a default config.ini, check if it's valid + if glob.conf.checkConfig() == False: + consoleHelper.printError() + consoleHelper.printColored("[!] Invalid config.ini. Please configure it properly", bcolors.RED) + consoleHelper.printColored("[!] Delete your config.ini to generate a default one", bcolors.RED) + sys.exit() + else: + consoleHelper.printDone() + + + # Connect to db + try: + consoleHelper.printNoNl("> Connecting to MySQL db... ") + glob.db = databaseHelper.db(glob.conf.config["db"]["host"], glob.conf.config["db"]["username"], glob.conf.config["db"]["password"], glob.conf.config["db"]["database"], int(glob.conf.config["db"]["pingtime"])) + consoleHelper.printDone() + except: + # Exception while connecting to db + consoleHelper.printError() + consoleHelper.printColored("[!] Error while connection to database. Please check your config.ini and run the server again", bcolors.RED) + raise + + # Load bancho_settings + try: + consoleHelper.printNoNl("> Loading bancho settings from DB... ") + glob.banchoConf = banchoConfig.banchoConfig() + consoleHelper.printDone() + except: + consoleHelper.printError() + consoleHelper.printColored("[!] Error while loading bancho_settings. Please make sure the table in DB has all the required rows", bcolors.RED) + raise + + # Initialize chat channels + consoleHelper.printNoNl("> Initializing chat channels... ") + glob.channels.loadChannels() + consoleHelper.printDone() + + # Start fokabot + consoleHelper.printNoNl("> Connecting FokaBot... ") + fokabot.connect() + consoleHelper.printDone() + + # Initialize user timeout check loop + try: + consoleHelper.printNoNl("> Initializing user timeout check loop... ") + glob.tokens.usersTimeoutCheckLoop(int(glob.conf.config["server"]["timeouttime"]), int(glob.conf.config["server"]["timeoutlooptime"])) + consoleHelper.printDone() + except: + consoleHelper.printError() + consoleHelper.printColored("[!] Error while initializing user timeout check loop", bcolors.RED) + consoleHelper.printColored("[!] Make sure that 'timeouttime' and 'timeoutlooptime' in config.ini are numbers", bcolors.RED) + raise + + # Localize warning + if(generalFunctions.stringToBool(glob.conf.config["server"]["localizeusers"]) == False): + consoleHelper.printColored("[!] Warning! users localization is disabled!", bcolors.YELLOW) + + # Get server parameters from config.ini + serverName = glob.conf.config["server"]["server"] + serverHost = glob.conf.config["server"]["host"] + serverPort = int(glob.conf.config["server"]["port"]) + serverOutputPackets = generalFunctions.stringToBool(glob.conf.config["server"]["outputpackets"]) + serverOutputRequestTime = generalFunctions.stringToBool(glob.conf.config["server"]["outputrequesttime"]) + + # Run server sanic way + if serverName == "tornado": + # Tornado server + consoleHelper.printColored("> Tornado listening for clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) + webServer = HTTPServer(WSGIContainer(app)) + webServer.listen(serverPort) + IOLoop.instance().start() + elif serverName == "flask": + # Flask server + # Get flask settings + flaskThreaded = generalFunctions.stringToBool(glob.conf.config["flask"]["threaded"]) + flaskDebug = generalFunctions.stringToBool(glob.conf.config["flask"]["debug"]) + flaskLoggerStatus = not generalFunctions.stringToBool(glob.conf.config["flask"]["logger"]) + + # Set flask debug mode and logger + app.debug = flaskDebug + flaskLogger.disabled = flaskLoggerStatus + + # Console output + if flaskDebug == False: + consoleHelper.printColored("> Flask listening for clients on {}.{}...".format(serverHost, serverPort), bcolors.GREEN) + else: + consoleHelper.printColored("> Flask "+bcolors.YELLOW+"(debug mode)"+bcolors.ENDC+" listening for clients on {}:{}...".format(serverHost, serverPort), bcolors.GREEN) + + # Run flask server + app.run(host=serverHost, port=serverPort, threaded=flaskThreaded) + else: + print(bcolors.RED+"[!] Unknown server. Please set the server key in config.ini to "+bcolors.ENDC+bcolors.YELLOW+"tornado"+bcolors.ENDC+bcolors.RED+" or "+bcolors.ENDC+bcolors.YELLOW+"flask"+bcolors.ENDC) + sys.exit() diff --git a/responseHelper.py b/responseHelper.py new file mode 100644 index 0000000..a2fbd8f --- /dev/null +++ b/responseHelper.py @@ -0,0 +1,47 @@ +import flask +import gzip + +def generateResponse(token, data = None): + """ + Return a flask response with required headers for osu! client, token and gzip compressed data + + token -- user token + data -- plain response body + return -- flask response + """ + + resp = flask.Response(gzip.compress(data, 6)) + resp.headers['cho-token'] = token + resp.headers['cho-protocol'] = '19' + resp.headers['Keep-Alive'] = 'timeout=5, max=100' + resp.headers['Connection'] = 'keep-alive' + resp.headers['Content-Type'] = 'text/html; charset=UTF-8' + resp.headers['Vary'] = 'Accept-Encoding' + resp.headers['Content-Encoding'] = 'gzip' + return resp + + +def HTMLResponse(): + """Return HTML bancho meme response""" + + html = "MA MAURO ESISTE?
"
+	html += "           _                 __
" + html += " (_) / /
" + html += " ______ __ ____ ____ / /____
" + html += " / ___/ / _ \\/ _ \\/ / _ \\
" + html += " / / / / /_) / /_) / / ____/
" + html += "/__/ /__/ .___/ .___/__/ \\_____/
" + html += " / / / /
" + html += " /__/ /__/
" + html += "PYTHON > ALL VERSION

" + html += "
" + html += " .. o .
" + html += " o.o o . o
" + html += " oo...
" + html += " __[]__
" + html += " phwr--> _\\:D/_/o_o_o_|__ u wot m8
" + html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/
" + html += " \\ . .. .. . /
" + html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
" + html += "

reverse engineering a protocol impossible to reverse engineer since always
we are actually reverse engineering bancho successfully. for the third time.
" + return html diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..b10df9a --- /dev/null +++ b/routes.py @@ -0,0 +1,52 @@ +""" +WIP feature that will come in the future. +Don't import +""" +import flask +import glob +import exceptions + +@app.route("/api/online-users-count") +def APIonlineUsersCount(): + return flask.jsonify({"count" : len(glob.tokens.tokens)-1}) + +@app.route("/api/user-info") +def APIonlineUsers(): + resp = {} + + try: + u = flask.request.args.get('u') + + # Username/userID + if u.isdigit(): + u = int(u) + else: + u = userHelper.getID(u) + if u == None: + raise exceptions.userNotFoundException + + # Make sure this user is online + userToken = glob.tokens.getTokenFromUserID(u) + if userToken == None: + raise exceptions.tokenNotFoundException + + # Build response dictionary + resp["response"] = "1" + resp[userToken.username] = { + "userID" : userToken.userID, + "actionID" : userToken.actionID, + "actionText" : userToken.actionText, + "actionMd5" : userToken.actionMd5, + "actionMods": userToken.actionMods, + "gameMode": userToken.gameMode, + "country": countryHelper.getCountryLetters(userToken.country), + "position": userToken.location, + "spectating": userToken.spectating, + "spectators": userToken.spectators + } + except exceptions.userNotFoundException: + resp["response"] = "-1" + except exceptions.tokenNotFoundException: + resp["response"] = "-2" + finally: + return flask.jsonify(resp) diff --git a/runserver.bat b/runserver.bat new file mode 100644 index 0000000..a041e09 --- /dev/null +++ b/runserver.bat @@ -0,0 +1,4 @@ +D: +cd D:\DevStuff\ripple-v15\c.ppy.sh +python pep.py +pause diff --git a/sendPrivateMessageEvent.py b/sendPrivateMessageEvent.py new file mode 100644 index 0000000..d2056df --- /dev/null +++ b/sendPrivateMessageEvent.py @@ -0,0 +1,47 @@ +import consoleHelper +import bcolors +import clientPackets +import serverPackets +import glob +import fokabot +import exceptions + +def handle(userToken, packetData): + """ + Event called when someone sends a private message + + userToken -- request user token + packetData -- request data bytes + """ + + try: + # Get usertoken username + username = userToken.username + + # Private message packet + packetData = clientPackets.sendPrivateMessage(packetData) + + if packetData["to"] == "FokaBot": + # FokaBot command check + fokaMessage = fokabot.fokabotResponse(username, packetData["to"], packetData["message"]) + if fokaMessage != False: + userToken.enqueue(serverPackets.sendMessage("FokaBot", username, fokaMessage)) + consoleHelper.printColored("> FokaBot>{}: {}".format(packetData["to"], str(fokaMessage.encode("UTF-8"))), bcolors.PINK) + else: + # Send packet message to target if it exists + token = glob.tokens.getTokenFromUsername(packetData["to"]) + if token == None: + raise exceptions.tokenNotFoundException() + + # Send message to target + token.enqueue(serverPackets.sendMessage(username, packetData["to"], packetData["message"])) + + # Send away message to sender if needed + if token.awayMessage != "": + userToken.enqueue(serverPackets.sendMessage(packetData["to"], username, "This user is away: {}".format(token.awayMessage))) + + # Console output + consoleHelper.printColored("> {}>{}: {}".format(username, packetData["to"], packetData["message"]), bcolors.PINK) + except exceptions.tokenNotFoundException: + # Token not found, user disconnected + consoleHelper.printColored("[!] {} tried to send a message to {}, but their token couldn't be found".format(username, packetData["to"]), bcolors.RED) diff --git a/sendPublicMessageEvent.py b/sendPublicMessageEvent.py new file mode 100644 index 0000000..a20d6e3 --- /dev/null +++ b/sendPublicMessageEvent.py @@ -0,0 +1,108 @@ +import exceptions +import clientPackets +import glob +import fokabot +import consoleHelper +import bcolors +import serverPackets + +def handle(userToken, packetData): + """ + Event called when someone sends a public message + + userToken -- request user token + packetData -- request data bytes + """ + + try: + # Get uesrToken data + userID = userToken.userID + username = userToken.username + userRank = userToken.rank + + # Public chat packet + packetData = clientPackets.sendPublicMessage(packetData) + + # Receivers + who = [] + + # Check #spectator + if packetData["to"] == "#spectator": + # Spectator channel + # Send this packet to every spectator and host + if userToken.spectating == 0: + # We have sent to send a message to our #spectator channel + targetToken = userToken + who = targetToken.spectators[:] + # No need to remove us because we are the host so we are not in spectators list + else: + # We have sent a message to someone else's #spectator + targetToken = glob.tokens.getTokenFromUserID(userToken.spectating) + who = targetToken.spectators[:] + + # Remove us + if userID in who: + who.remove(userID) + + # Add host + who.append(targetToken.userID) + elif packetData["to"] == "#multiplayer": + # Multiplayer Channel + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Create targets list + who = [] + for i in range(0,16): + uid = match.slots[i]["userID"] + if uid > -1 and uid != userID: + who.append(uid) + else: + # Standard channel + # Make sure the channel exists + if packetData["to"] not in glob.channels.channels: + raise exceptions.channelUnknownException + + # Make sure the channel is not in moderated mode + if glob.channels.channels[packetData["to"]].moderated == True and userRank <= 2: + raise exceptions.channelModeratedException + + # Make sure we have write permissions + if glob.channels.channels[packetData["to"]].publicWrite == False and userRank <= 2: + raise exceptions.channelNoPermissionsException + + # Send this packet to everyone in that channel except us + who = glob.channels.channels[packetData["to"]].getConnectedUsers()[:] + if userID in who: + who.remove(userID) + + + # Send packet to required users + glob.tokens.multipleEnqueue(serverPackets.sendMessage(username, packetData["to"], packetData["message"]), who, False) + + # Fokabot command check + fokaMessage = fokabot.fokabotResponse(username, packetData["to"], packetData["message"]) + if fokaMessage != False: + who.append(userID) + glob.tokens.multipleEnqueue(serverPackets.sendMessage("FokaBot", packetData["to"], fokaMessage), who, False) + consoleHelper.printColored("> FokaBot@{}: {}".format(packetData["to"], str(fokaMessage.encode("UTF-8"))), bcolors.PINK) + + # Console output + consoleHelper.printColored("> {}@{}: {}".format(username, packetData["to"], str(packetData["message"].encode("UTF-8"))), bcolors.PINK) + except exceptions.channelModeratedException: + consoleHelper.printColored("[!] {} tried to send a message to a channel that is in moderated mode ({})".format(username, packetData["to"]), bcolors.RED) + except exceptions.channelUnknownException: + consoleHelper.printColored("[!] {} tried to send a message to an unknown channel ({})".format(username, packetData["to"]), bcolors.RED) + except exceptions.channelNoPermissionsException: + consoleHelper.printColored("[!] {} tried to send a message to channel {}, but they have no write permissions".format(username, packetData["to"]), bcolors.RED) diff --git a/serverPackets.py b/serverPackets.py new file mode 100644 index 0000000..2cd6c2c --- /dev/null +++ b/serverPackets.py @@ -0,0 +1,273 @@ +""" Contains functions used to write specific server packets to byte streams """ +import packetHelper +import dataTypes +import userHelper +import glob +import userRanks +import packetIDs +import slotStatuses +import matchModModes +import random + +""" Login errors packets +(userID packets derivates) """ +def loginFailed(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.sInt32]]) + +def forceUpdate(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-2, dataTypes.sInt32]]) + +def loginBanned(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-3, dataTypes.sInt32]]) + +def loginError(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-5, dataTypes.sInt32]]) + +def needSupporter(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-6, dataTypes.sInt32]]) + + +""" Login packets """ +def userID(uid): + return packetHelper.buildPacket(packetIDs.server_userID, [[uid, dataTypes.sInt32]]) + +def silenceEndTime(seconds): + return packetHelper.buildPacket(packetIDs.server_silenceEnd, [[seconds, dataTypes.uInt32]]) + +def protocolVersion(version = 19): + return packetHelper.buildPacket(packetIDs.server_protocolVersion, [[version, dataTypes.uInt32]]) + +def mainMenuIcon(icon): + return packetHelper.buildPacket(packetIDs.server_mainMenuIcon, [[icon, dataTypes.string]]) + +def userSupporterGMT(supporter, GMT): + result = 1 + if supporter == True: + result += 4 + if GMT == True: + result += 2 + return packetHelper.buildPacket(packetIDs.server_supporterGMT, [[result, dataTypes.uInt32]]) + +def friendList(userID): + friendsData = [] + + # Get friend IDs from db + friends = userHelper.getFriendList(userID) + + # Friends number + friendsData.append([len(friends), dataTypes.uInt16]) + + # Add all friend user IDs to friendsData + for i in friends: + friendsData.append([i, dataTypes.sInt32]) + + return packetHelper.buildPacket(packetIDs.server_friendsList, friendsData) + +def onlineUsers(): + onlineUsersData = [] + + users = glob.tokens.tokens + + # Users number + onlineUsersData.append([len(users), dataTypes.uInt16]) + + # Add all users user IDs to onlineUsersData + for _,value in users.items(): + onlineUsersData.append([value.userID, dataTypes.sInt32]) + + return packetHelper.buildPacket(packetIDs.server_userPresenceBundle, onlineUsersData) + + +""" Users packets """ +def userLogout(userID): + return packetHelper.buildPacket(packetIDs.server_userLogout, [[userID, dataTypes.sInt32], [0, dataTypes.byte]]) + +def userPanel(userID): + # Get user data + userToken = glob.tokens.getTokenFromUserID(userID) + username = userHelper.getUsername(userID) + timezone = 24 # TODO: Timezone + country = userToken.getCountry() + gameRank = userHelper.getGameRank(userID, userToken.gameMode) + latitude = userToken.getLatitude() + longitude = userToken.getLongitude() + + # Get username color according to rank + # Only admins and normal users are currently supported + rank = userHelper.getRankPrivileges(userID) + if username == "FokaBot": + userRank = userRanks.MOD + elif rank == 4: + userRank = userRanks.ADMIN + elif rank == 3: + userRank = userRank.MOD + elif rank == 2: + userRank = userRanks.SUPPORTER + else: + userRank = userRanks.NORMAL + + + return packetHelper.buildPacket(packetIDs.server_userPanel, + [ + [userID, dataTypes.sInt32], + [username, dataTypes.string], + [timezone, dataTypes.byte], + [country, dataTypes.byte], + [userRank, dataTypes.byte], + [longitude, dataTypes.ffloat], + [latitude, dataTypes.ffloat], + [gameRank, dataTypes.uInt32] + ]) + + +def userStats(userID): + # Get userID's token from tokens list + userToken = glob.tokens.getTokenFromUserID(userID) + + # Get stats from DB + # TODO: Caching system + rankedScore = userHelper.getRankedScore(userID, userToken.gameMode) + accuracy = userHelper.getAccuracy(userID, userToken.gameMode)/100 + playcount = userHelper.getPlaycount(userID, userToken.gameMode) + totalScore = userHelper.getTotalScore(userID, userToken.gameMode) + gameRank = userHelper.getGameRank(userID, userToken.gameMode) + pp = int(userHelper.getPP(userID, userToken.gameMode)) + + return packetHelper.buildPacket(packetIDs.server_userStats, + [ + [userID, dataTypes.uInt32], + [userToken.actionID, dataTypes.byte], + [userToken.actionText, dataTypes.string], + [userToken.actionMd5, dataTypes.string], + [userToken.actionMods, dataTypes.sInt32], + [userToken.gameMode, dataTypes.byte], + [0, dataTypes.sInt32], + [rankedScore, dataTypes.uInt64], + [accuracy, dataTypes.ffloat], + [playcount, dataTypes.uInt32], + [totalScore, dataTypes.uInt64], + [gameRank, dataTypes.uInt32], + [pp, dataTypes.uInt16] + ]) + + +""" Chat packets """ +def sendMessage(fro, to, message): + return packetHelper.buildPacket(packetIDs.server_sendMessage, [[fro, dataTypes.string], [message, dataTypes.string], [to, dataTypes.string], [userHelper.getID(fro), dataTypes.sInt32]]) + +def channelJoinSuccess(userID, chan): + return packetHelper.buildPacket(packetIDs.server_channelJoinSuccess, [[chan, dataTypes.string]]) + +def channelInfo(chan): + channel = glob.channels.channels[chan] + return packetHelper.buildPacket(packetIDs.server_channelInfo, [[chan, dataTypes.string], [channel.description, dataTypes.string], [channel.getConnectedUsersCount(), dataTypes.uInt16]]) + +def channelInfoEnd(): + return packetHelper.buildPacket(packetIDs.server_channelInfoEnd, [[0, dataTypes.uInt32]]) + +def channelKicked(chan): + return packetHelper.buildPacket(packetIDs.server_channelKicked, [[chan, dataTypes.string]]) + + +""" Spectator packets """ +def addSpectator(userID): + return packetHelper.buildPacket(packetIDs.server_spectatorJoined, [[userID, dataTypes.sInt32]]) + +def removeSpectator(userID): + return packetHelper.buildPacket(packetIDs.server_spectatorLeft, [[userID, dataTypes.sInt32]]) + +def spectatorFrames(data): + return packetHelper.buildPacket(packetIDs.server_spectateFrames, [[data, dataTypes.bbytes]]) + +def noSongSpectator(userID): + return packetHelper.buildPacket(packetIDs.server_spectatorCantSpectate, [[userID, dataTypes.sInt32]]) + + +""" Multiplayer Packets """ +def createMatch(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + return packetHelper.buildPacket(packetIDs.server_newMatch, match.getMatchData()) + + +def updateMatch(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + return packetHelper.buildPacket(packetIDs.server_updateMatch, match.getMatchData()) + + +def matchStart(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + return packetHelper.buildPacket(packetIDs.server_matchStart, match.getMatchData()) + + +def disposeMatch(matchID): + return packetHelper.buildPacket(packetIDs.server_disposeMatch, [[matchID, dataTypes.uInt16]]) + +def matchJoinSuccess(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + data = packetHelper.buildPacket(packetIDs.server_matchJoinSuccess, match.getMatchData()) + return data + +def matchJoinFail(): + return packetHelper.buildPacket(packetIDs.server_matchJoinFail) + +def changeMatchPassword(newPassword): + return packetHelper.buildPacket(packetIDs.server_matchChangePassword, [[newPassword, dataTypes.string]]) + +def allPlayersLoaded(): + return packetHelper.buildPacket(packetIDs.server_matchAllPlayersLoaded) + +def playerSkipped(userID): + return packetHelper.buildPacket(packetIDs.server_matchPlayerSkipped, [[userID, dataTypes.sInt32]]) + +def allPlayersSkipped(): + return packetHelper.buildPacket(packetIDs.server_matchSkip) + +def matchFrames(slotID, data): + return packetHelper.buildPacket(packetIDs.server_matchScoreUpdate, [[data[7:11], dataTypes.bbytes], [slotID, dataTypes.byte], [data[12:], dataTypes.bbytes]]) + +def matchComplete(): + return packetHelper.buildPacket(packetIDs.server_matchComplete) + +def playerFailed(slotID): + return packetHelper.buildPacket(packetIDs.server_matchPlayerFailed, [[slotID, dataTypes.uInt32]]) + +def matchTransferHost(): + return packetHelper.buildPacket(packetIDs.server_matchTransferHost) + +""" Other packets """ +def notification(message): + return packetHelper.buildPacket(packetIDs.server_notification, [[message, dataTypes.string]]) + +def jumpscare(message): + return packetHelper.buildPacket(packetIDs.server_jumpscare, [[message, dataTypes.string]]) + +def banchoRestart(msUntilReconnection): + return packetHelper.buildPacket(packetIDs.server_restart, [[msUntilReconnection, dataTypes.uInt32]]) + + +""" WIP Packets """ +def getAttention(): + return packetHelper.buildPacket(packetIDs.server_getAttention) + +def packet80(): + return packetHelper.buildPacket(packetIDs.server_topBotnet) diff --git a/setAwayMessageEvent.py b/setAwayMessageEvent.py new file mode 100644 index 0000000..76e8f2a --- /dev/null +++ b/setAwayMessageEvent.py @@ -0,0 +1,20 @@ +import clientPackets +import serverPackets + +def handle(userToken, packetData): + # get token data + username = userToken.username + + # Read packet data + packetData = clientPackets.setAwayMessage(packetData) + + # Set token away message + userToken.setAwayMessage(packetData["awayMessage"]) + + # Send private message from fokabot + if packetData["awayMessage"] == "": + fokaMessage = "Your away message has been reset" + else: + fokaMessage = "Your away message is now: {}".format(packetData["awayMessage"]) + userToken.enqueue(serverPackets.sendMessage("FokaBot", username, fokaMessage)) + print("{} has changed their away message to: {}".format(username, packetData["awayMessage"])) diff --git a/slotStatuses.py b/slotStatuses.py new file mode 100644 index 0000000..be36c78 --- /dev/null +++ b/slotStatuses.py @@ -0,0 +1,8 @@ +free = 1 +locked = 2 +notReady = 4 +ready = 8 +noMap = 16 +playing = 32 +occupied = 124 +playingQuit = 128 diff --git a/spectateFramesEvent.py b/spectateFramesEvent.py new file mode 100644 index 0000000..7522392 --- /dev/null +++ b/spectateFramesEvent.py @@ -0,0 +1,33 @@ +import glob +import consoleHelper +import bcolors +import serverPackets +import exceptions + +def handle(userToken, packetData): + # get token data + userID = userToken.userID + + # Send spectator frames to every spectator + consoleHelper.printColored("> {}'s spectators: {}".format(str(userID), str(userToken.spectators)), bcolors.BLUE) + for i in userToken.spectators: + # Send to every user but host + if i != userID: + try: + # Get spectator token object + spectatorToken = glob.tokens.getTokenFromUserID(i) + + # Make sure the token exists + if spectatorToken == None: + raise exceptions.stopSpectating + + # Make sure this user is spectating us + if spectatorToken.spectating != userID: + raise exceptions.stopSpectating + + # Everything seems fine, send spectator frames to this spectator + spectatorToken.enqueue(serverPackets.spectatorFrames(packetData[7:])) + except exceptions.stopSpectating: + # Remove this user from spectators + userToken.removeSpectator(i) + userToken.enqueue(serverPackets.removeSpectator(i)) diff --git a/startSpectatingEvent.py b/startSpectatingEvent.py new file mode 100644 index 0000000..6e8414d --- /dev/null +++ b/startSpectatingEvent.py @@ -0,0 +1,51 @@ +import consoleHelper +import bcolors +import clientPackets +import serverPackets +import exceptions +import glob +import userHelper + +def handle(userToken, packetData): + try: + # Get usertoken data + userID = userToken.userID + username = userToken.username + + # Start spectating packet + packetData = clientPackets.startSpectating(packetData) + + # Stop spectating old user if needed + if userToken.spectating != 0: + oldTargetToken = glob.tokens.getTokenFromUserID(userToken.spectating) + oldTargetToken.enqueue(serverPackets.removeSpectator(userID)) + userToken.stopSpectating() + + # Start spectating new user + userToken.startSpectating(packetData["userID"]) + + # Get host token + targetToken = glob.tokens.getTokenFromUserID(packetData["userID"]) + if targetToken == None: + raise exceptions.tokenNotFoundException + + # Add us to host's spectators + targetToken.addSpectator(userID) + + # Send spectator join packet to host + targetToken.enqueue(serverPackets.addSpectator(userID)) + + # Join #spectator channel + userToken.enqueue(serverPackets.channelJoinSuccess(userID, "#spectator")) + + if len(targetToken.spectators) == 1: + # First spectator, send #spectator join to host too + targetToken.enqueue(serverPackets.channelJoinSuccess(userID, "#spectator")) + + # Console output + consoleHelper.printColored("> {} are spectating {}".format(username, userHelper.getUsername(packetData["userID"])), bcolors.PINK) + consoleHelper.printColored("> {}'s spectators: {}".format(str(packetData["userID"]), str(targetToken.spectators)), bcolors.BLUE) + except exceptions.tokenNotFoundException: + # Stop spectating if token not found + consoleHelper.printColored("[!] Spectator start: token not found", bcolors.RED) + userToken.stopSpectating() diff --git a/stopSpectatingEvent.py b/stopSpectatingEvent.py new file mode 100644 index 0000000..acd9761 --- /dev/null +++ b/stopSpectatingEvent.py @@ -0,0 +1,31 @@ +import consoleHelper +import bcolors +import glob +import serverPackets +import exceptions + +def handle(userToken, _): + try: + # get user token data + userID = userToken.userID + username = userToken.username + + # Remove our userID from host's spectators + target = userToken.spectating + targetToken = glob.tokens.getTokenFromUserID(target) + if targetToken == None: + raise exceptions.tokenNotFoundException + targetToken.removeSpectator(userID) + + # Send the spectator left packet to host + targetToken.enqueue(serverPackets.removeSpectator(userID)) + + # Console output + # TODO: Move messages in stop spectating + consoleHelper.printColored("> {} are no longer spectating whoever they were spectating".format(username), bcolors.PINK) + consoleHelper.printColored("> {}'s spectators: {}".format(str(target), str(targetToken.spectators)), bcolors.BLUE) + except exceptions.tokenNotFoundException: + consoleHelper.printColored("[!] Spectator stop: token not found", bcolors.RED) + finally: + # Set our spectating user to 0 + userToken.stopSpectating() diff --git a/systemHelper.py b/systemHelper.py new file mode 100644 index 0000000..b381487 --- /dev/null +++ b/systemHelper.py @@ -0,0 +1,91 @@ +import glob +import serverPackets +import psutil +import os +import sys + +import consoleHelper +import bcolors +import threading +import signal + +def runningUnderUnix(): + """ + Get if the server is running under UNIX or NT + + return --- True if running under UNIX, otherwise False + """ + + return True if os.name == "posix" else False + + +def scheduleShutdown(sendRestartTime, restart, message = ""): + """ + Schedule a server shutdown/restart + + sendRestartTime -- time (seconds) to wait before sending server restart packets to every client + restart -- if True, server will restart. if False, server will shudown + message -- if set, send that message to every client to warn about the shutdown/restart + """ + + # Console output + consoleHelper.printColored("[!] Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+20), bcolors.PINK) + consoleHelper.printColored("[!] Sending server restart packets in {} seconds...".format(sendRestartTime), bcolors.PINK) + + # Send notification if set + if message != "": + glob.tokens.enqueueAll(serverPackets.notification(message)) + + # Schedule server restart packet + threading.Timer(sendRestartTime, glob.tokens.enqueueAll, [serverPackets.banchoRestart(50000)]).start() + glob.restarting = True + + # Restart/shutdown + if restart: + action = restartServer + else: + action = shutdownServer + + # Schedule actual server shutdown/restart 20 seconds after server restart packet, so everyone gets it + threading.Timer(sendRestartTime+20, action).start() + + +def restartServer(): + """Restart pep.py script""" + print("> Restarting pep.py...") + os.execv(sys.executable, [sys.executable] + sys.argv) + + +def shutdownServer(): + """Shutdown pep.py""" + print("> Shutting down pep.py...") + sig = signal.SIGKILL if runningUnderUnix() else signal.CTRL_C_EVENT + os.kill(os.getpid(), sig) + + +def getSystemInfo(): + """ + Get a dictionary with some system/server info + + return -- ["unix", "connectedUsers", "webServer", "cpuUsage", "totalMemory", "usedMemory", "loadAverage"] + """ + + data = {} + + # Get if server is running under unix/nt + data["unix"] = runningUnderUnix() + + # General stats + data["connectedUsers"] = len(glob.tokens.tokens) + data["webServer"] = glob.conf.config["server"]["server"] + data["cpuUsage"] = psutil.cpu_percent() + data["totalMemory"] = "{0:.2f}".format(psutil.virtual_memory()[0]/1074000000) + data["usedMemory"] = "{0:.2f}".format(psutil.virtual_memory()[3]/1074000000) + + # Unix only stats + if data["unix"] == True: + data["loadAverage"] = os.getloadavg() + else: + data["loadAverage"] = (0,0,0) + + return data diff --git a/tokenList.py b/tokenList.py new file mode 100644 index 0000000..39a6eac --- /dev/null +++ b/tokenList.py @@ -0,0 +1,165 @@ +import osuToken +import time +import threading +import logoutEvent + +class tokenList: + """ + List of connected osu tokens + + tokens -- dictionary. key: token string, value: token object + """ + + tokens = {} + + def addToken(self, __userID): + """ + Add a token object to tokens list + + __userID -- user id associated to that token + return -- token object + """ + + newToken = osuToken.token(__userID) + self.tokens[newToken.token] = newToken + return newToken + + def deleteToken(self, __token): + """ + Delete a token from token list if it exists + + __token -- token string + """ + + if __token in self.tokens: + self.tokens.pop(__token) + + + def getUserIDFromToken(self, __token): + """ + Get user ID from a token + + __token -- token to find + + return: false if not found, userID if found + """ + + # Make sure the token exists + if __token not in self.tokens: + return False + + # Get userID associated to that token + return self.tokens[__token].userID + + + def getTokenFromUserID(self, __userID): + """ + Get token from a user ID + + __userID -- user ID to find + return -- False if not found, token object if found + """ + + # Make sure the token exists + for _, value in self.tokens.items(): + if value.userID == __userID: + return value + + # Return none if not found + return None + + + def getTokenFromUsername(self, __username): + """ + Get token from a username + + __username -- username to find + return -- False if not found, token object if found + """ + + # lowercase + who = __username.lower() + + # Make sure the token exists + for _, value in self.tokens.items(): + if value.username.lower() == who: + return value + + # Return none if not found + return None + + + def deleteOldTokens(self, __userID): + """ + Delete old userID's tokens if found + + __userID -- tokens associated to this user will be deleted + """ + + # Delete older tokens + for key, value in self.tokens.items(): + if value.userID == __userID: + # Delete this token from the dictionary + self.tokens.pop(key) + + # break or items() function throws errors + break + + + def multipleEnqueue(self, __packet, __who, __but = False): + """ + Enqueue a packet to multiple users + + __packet -- packet bytes to enqueue + __who -- userIDs array + __but -- if True, enqueue to everyone but users in __who array + """ + + for _, value in self.tokens.items(): + shouldEnqueue = False + if value.userID in __who and not __but: + shouldEnqueue = True + elif value.userID not in __who and __but: + shouldEnqueue = True + + if shouldEnqueue: + value.enqueue(__packet) + + + + def enqueueAll(self, __packet): + """ + Enqueue packet(s) to every connected user + + __packet -- packet bytes to enqueue + """ + + for _, value in self.tokens.items(): + value.enqueue(__packet) + + def usersTimeoutCheckLoop(self, __timeoutTime = 100, __checkTime = 100): + """ + Deletes all timed out users. + If called once, will recall after __checkTime seconds and so on, forever + CALL THIS FUNCTION ONLY ONCE! + + __timeoutTime - seconds of inactivity required to disconnect someone (Default: 100) + __checkTime - seconds between loops (Default: 100) + """ + + timedOutTokens = [] # timed out users + timeoutLimit = time.time()-__timeoutTime + for key, value in self.tokens.items(): + # Check timeout (fokabot is ignored) + if value.pingTime < timeoutLimit and value.userID != 999: + # That user has timed out, add to disconnected tokens + # We can't delete it while iterating or items() throws an error + timedOutTokens.append(key) + + # Delete timed out users from self.tokens + # i is token string (dictionary key) + for i in timedOutTokens: + logoutEvent.handle(self.tokens[i], None) + + # Schedule a new check (endless loop) + threading.Timer(__checkTime, self.usersTimeoutCheckLoop, [__timeoutTime, __checkTime]).start() diff --git a/userHelper.py b/userHelper.py new file mode 100644 index 0000000..7d0aa41 --- /dev/null +++ b/userHelper.py @@ -0,0 +1,268 @@ +import passwordHelper +import gameModes +import glob + +def getID(username): + """ + Get username's user ID + + db -- database connection + username -- user + return -- user id or False + """ + + # Get user ID from db + userID = glob.db.fetch("SELECT id FROM users WHERE username = ?", [username]) + + # Make sure the query returned something + if userID == None: + return False + + # Return user ID + return userID["id"] + + +def checkLogin(userID, password): + """ + Check userID's login with specified password + + db -- database connection + userID -- user id + password -- plain md5 password + return -- True or False + """ + + # Get password data + passwordData = glob.db.fetch("SELECT password_md5, salt, password_version FROM users WHERE id = ?", [userID]) + + # Make sure the query returned something + if passwordData == None: + return False + + + # Return valid/invalid based on the password version. + if passwordData["password_version"] == 2: + return passwordHelper.checkNewPassword(password, passwordData["password_md5"]) + if passwordData["password_version"] == 1: + ok = passwordHelper.checkOldPassword(password, passwordData["salt"], passwordData["password_md5"]) + if not ok: return False + newpass = passwordHelper.genBcrypt(password) + glob.db.execute("UPDATE users SET password_md5=?, salt='', password_version='2' WHERE id = ?", [newpass, userID]) + + +def exists(userID): + """ + Check if userID exists + + userID -- user ID to check + return -- bool + """ + + result = glob.db.fetch("SELECT id FROM users WHERE id = ?", [userID]) + if result == None: + return False + else: + return True + +def getAllowed(userID): + """ + Get allowed status for userID + + db -- database connection + userID -- user ID + return -- allowed int + """ + + return glob.db.fetch("SELECT allowed FROM users WHERE id = ?", [userID])["allowed"] + + +def getRankPrivileges(userID): + """ + This returns rank **(PRIVILEGES)**, not game rank (like #1337) + If you want to get that rank, user getUserGameRank instead + """ + + return glob.db.fetch("SELECT rank FROM users WHERE id = ?", [userID])["rank"] + + +def getSilenceEnd(userID): + """ + Get userID's **ABSOLUTE** silence end UNIX time + Remember to subtract time.time() to get the actual silence time + + userID -- userID + return -- UNIX time + """ + + return glob.db.fetch("SELECT silence_end FROM users WHERE id = ?", [userID])["silence_end"] + + +def silence(userID, silenceEndTime, silenceReason): + """ + Set userID's **ABSOLUTE** silence end UNIX time + Remember to add time.time() to the silence length + + userID -- userID + silenceEndtime -- UNIX time when the silence ends + silenceReason -- Silence reason shown on website + """ + + glob.db.execute("UPDATE users SET silence_end = ?, silence_reason = ? WHERE id = ?", [silenceEndTime, silenceReason, userID]) + +def getRankedScore(userID, gameMode): + """ + Get userID's ranked score relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- ranked score + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT ranked_score_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["ranked_score_"+modeForDB] + + +def getTotalScore(userID, gameMode): + """ + Get userID's total score relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- total score + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT total_score_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["total_score_"+modeForDB] + + +def getAccuracy(userID, gameMode): + """ + Get userID's average accuracy relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- accuracy + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT avg_accuracy_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["avg_accuracy_"+modeForDB] + + +def getGameRank(userID, gameMode): + """ + Get userID's **in-game rank** (eg: #1337) relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- game rank + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + result = glob.db.fetch("SELECT position FROM leaderboard_"+modeForDB+" WHERE user = ?", [userID]) + if result == None: + return 0 + else: + return result["position"] + + +def getPlaycount(userID, gameMode): + """ + Get userID's playcount relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- playcount + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT playcount_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["playcount_"+modeForDB] + + +def getUsername(userID): + """ + Get userID's username + + userID -- userID + return -- username + """ + + return glob.db.fetch("SELECT username FROM users WHERE id = ?", [userID])["username"] + + +def getFriendList(userID): + """ + Get userID's friendlist + + userID -- userID + return -- list with friends userIDs. [0] if no friends. + """ + + # Get friends from db + friends = glob.db.fetchAll("SELECT user2 FROM users_relationships WHERE user1 = ?", [userID]) + + if friends == None or len(friends) == 0: + # We have no friends, return 0 list + return [0] + else: + # Get only friends + friends = [i["user2"] for i in friends] + + # Return friend IDs + return friends + + +def addFriend(userID, friendID): + """ + Add friendID to userID's friend list + + userID -- user + friendID -- new friend + """ + + # Make sure we aren't adding us to our friends + if userID == friendID: + return + + # check user isn't already a friend of ours + if glob.db.fetch("SELECT id FROM users_relationships WHERE user1 = ? AND user2 = ?", [userID, friendID]) != None: + return + + # Set new value + glob.db.execute("INSERT INTO users_relationships (user1, user2) VALUES (?, ?)", [userID, friendID]) + + +def removeFriend(userID, friendID): + """ + Remove friendID from userID's friend list + + userID -- user + friendID -- old friend + """ + + # Delete user relationship. We don't need to check if the relationship was there, because who gives a shit, + # if they were not friends and they don't want to be anymore, be it. ¯\_(ツ)_/¯ + glob.db.execute("DELETE FROM users_relationships WHERE user1 = ? AND user2 = ?", [userID, friendID]) + + +def getCountry(userID): + """ + Get userID's country **(two letters)**. + Use countryHelper.getCountryID with what that function returns + to get osu! country ID relative to that user + + userID -- user + return -- country code (two letters) + """ + + return glob.db.fetch("SELECT country FROM users_stats WHERE id = ?", [userID])["country"] + +def getPP(userID, gameMode): + """ + Get userID's PP relative to gameMode + + userID -- user + return -- gameMode number + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT pp_{} FROM users_stats WHERE id = ?".format(modeForDB), [userID])["pp_{}".format(modeForDB)] diff --git a/userRanks.py b/userRanks.py new file mode 100644 index 0000000..923bede --- /dev/null +++ b/userRanks.py @@ -0,0 +1,9 @@ +"""Bancho user ranks""" +# TODO: Uppercase, maybe? +NORMAL = 0 +PLAYER = 1 +SUPPORTER = 4 +MOD = 6 +PEPPY = 8 +ADMIN = 16 +TOURNAMENTSTAFF = 32