def getRawReplayS3(scoreID): scoreID = int(scoreID) if not glob.conf.s3_enabled: log.warning("S3 is disabled! Using failed local") return _getRawReplayFailedLocal(scoreID) fileName = "replay_{}.osr".format(scoreID) log.debug("Downloading {} from s3".format(fileName)) with io.BytesIO() as f: bucket = s3.getReadReplayBucketName(scoreID) try: glob.threadScope.s3.download_fileobj(bucket, fileName, f) except ClientError as e: # 404 -> no such key # 400 -> no such bucket code = e.response["Error"]["Code"] if code in ("404", "400"): log.warning( "S3 replay returned {}, trying to get from failed replays". format(code)) if code == "400": sentry.captureMessage( "Invalid S3 replays bucket ({})! (got error 400)". format(bucket)) return _getRawReplayFailedLocal(scoreID) raise f.seek(0) return f.read()
def asyncGet(self): try: # Get request ip ip = self.getRequestIP() # Check arguments if not requestsManager.checkArguments(self.request.arguments, ["c", "u", "h"]): raise exceptions.invalidArgumentsException(MODULE_NAME) # Get arguments username = self.get_argument("u") password = self.get_argument("h") replayID = self.get_argument("c") # Login check userID = userUtils.getID(username) if userID == 0: raise exceptions.loginFailedException(MODULE_NAME, userID) if not userUtils.checkLogin(userID, password, ip): raise exceptions.loginFailedException(MODULE_NAME, username) if userUtils.check2FA(userID, ip): raise exceptions.need2FAException(MODULE_NAME, username, ip) # Get user ID replayData = glob.db.fetch( "SELECT scores.*, users.username AS uname FROM scores LEFT JOIN users ON scores.userid = users.id WHERE scores.id = %s", [replayID]) if replayData == None: replayData = glob.db.fetch( "SELECT scores_relax.*, users.username AS uname FROM scores_relax LEFT JOIN users ON scores_relax.userid = users.id WHERE scores_relax.id = %s", [replayID]) fileName = "{}_relax/replay_{}.osr".format( glob.conf.config["server"]["replayspath"], replayID) else: fileName = "{}/replay_{}.osr".format( glob.conf.config["server"]["replayspath"], replayID) # Increment 'replays watched by others' if needed if replayData is not None: if username != replayData["uname"]: userUtils.incrementReplaysWatched(replayData["userid"], replayData["play_mode"]) # Serve replay log.info("Serving replay_{}.osr".format(replayID)) if os.path.isfile(fileName): with open(fileName, "rb") as f: fileContent = f.read() self.write(fileContent) else: log.warning("Replay {} doesn't exist".format(replayID)) self.write("") except exceptions.invalidArgumentsException: pass except exceptions.need2FAException: pass except exceptions.loginFailedException: pass
def enqueue(self, bytes_): """ Add bytes (packets) to queue :param bytes_: (packet) bytes to enqueue """ try: # Acquire the buffer lock self._bufferLock.acquire() # Never enqueue for IRC clients or Bot #if self.irc or self.userID < 999: if self.irc or self.userID == 999: return # Avoid memory leaks if len(bytes_) < 10 * 10**6: self.queue += bytes_ else: log.warning( "{}'s packets buffer is above 10M!! Lost some data!". format(self.username)) finally: # Release the buffer lock self._bufferLock.release()
def updateCachedStats(self): """ Update all cached stats for this token :return: """ stats = userUtils.getUserStats(self.userID, self.gameMode) stats_relax = userUtils.getUserStatsRx(self.userID, self.gameMode) log.debug(str(stats)) if stats is None: log.warning("Stats query returned None") return if self.relaxing: self.gameRank = stats_relax["gameRank"] self.pp = stats_relax["pp"] self.rankedScore = stats_relax["rankedScore"] self.accuracy = stats_relax["accuracy"] / 100 self.playcount = stats_relax["playcount"] self.totalScore = stats_relax["totalScore"] else: self.gameRank = stats["gameRank"] self.pp = stats["pp"] self.rankedScore = stats["rankedScore"] self.accuracy = stats["accuracy"] / 100 self.playcount = stats["playcount"] self.totalScore = stats["totalScore"]
def updateCachedStats(self): """ Update all cached stats for this token :return: """ stats = [ userUtils.getUserStats(self.userID, self.gameMode), userUtils.getUserStatsRelax(self.userID, self.gameMode) ] if features.RANKING_SCOREV2: stats.append(userUtils.getUserStatsAlt(self.userID, self.gameMode)) if stats is None: log.warning("Stats query returned None") return if self.specialMode in range(len(stats)): stats = stats[self.specialMode] else: stats = stats[0] self.gameRank = stats["gameRank"] self.pp = stats["pp"] self.rankedScore = stats["rankedScore"] self.accuracy = stats["accuracy"] / 100.0 self.playcount = stats["playcount"] self.totalScore = stats["totalScore"]
def osuApiRequest(request, params, getFirst=True): """ Send a request to osu!api. request -- request type, string (es: get_beatmaps) params -- GET parameters, without api key or trailing ?/& (es: h=a5b99395a42bd55bc5eb1d2411cbdf8b&limit=10) return -- dictionary with json response if success, None if failed or empty response. """ # Make sure osuapi is enabled if not generalUtils.stringToBool(glob.conf.config["osuapi"]["enable"]): log.warning("osu!api is disabled") return None # Api request resp = None try: finalURL = "{}/api/{}?k={}&{}".format(glob.conf.config["osuapi"]["apiurl"], request, glob.conf.config["osuapi"]["apikey"], params) log.debug(finalURL) resp = requests.get(finalURL, timeout=5).text data = json.loads(resp) if getFirst: if len(data) >= 1: resp = data[0] else: resp = None else: resp = data finally: glob.dog.increment(glob.DATADOG_PREFIX+".osu_api.requests") log.debug(str(resp).encode("utf-8")) return resp
def handle(userToken, packetData): # read packet data packetData = clientPackets.joinMatch(packetData) matchID = packetData["matchID"] password = packetData["password"] # Get match from ID try: # Make sure the match exists if matchID not in glob.matches.matches: return # Hash password if needed # if password != "": # password = generalUtils.stringMd5(password) # Check password with glob.matches.matches[matchID] as match: if match.matchPassword != "" and match.matchPassword != password: raise exceptions.matchWrongPasswordException() # Password is correct, join match userToken.joinMatch(matchID) except exceptions.matchWrongPasswordException: userToken.enqueue(serverPackets.matchJoinFail()) log.warning( "{} has tried to join a mp room, but he typed the wrong password". format(userToken.username))
def report(fro, chan, message): msg = "" try: # TODO: Rate limit # Get username, report reason and report info target, reason, additionalInfo = message[0], message[1], message[2] target = chat.fixUsernameForBancho(target) # Make sure the target is not foka if target == glob.BOT_NAME: raise exceptions.invalidUserException() # Make sure the user exists targetID = userUtils.getID(target) if targetID == 0: raise exceptions.userNotFoundException() # Make sure that the user has specified additional info if report reason is 'Other' if reason.lower() == "other" and not additionalInfo: raise exceptions.missingReportInfoException() # Get the token if possible chatlog = "" token = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True) if token is not None: chatlog = token.getMessagesBufferString() # Everything is fine, submit report glob.db.execute( "INSERT INTO reports (id, from_uid, to_uid, reason, chatlog, time, assigned) VALUES (NULL, %s, %s, %s, %s, %s, 0)", [userUtils.getID(fro), targetID, "{reason} - ingame {info}".format(reason=reason, info="({})".format( additionalInfo) if additionalInfo is not None else ""), chatlog, int(time.time())]) msg = "You've reported {target} for {reason}{info}. A Community Manager will check your report as soon as possible. Every !report message you may see in chat wasn't sent to anyone, so nobody in chat, but admins, know about your report. Thank you for reporting!".format( target=target, reason=reason, info="" if additionalInfo is None else " (" + additionalInfo + ")") adminMsg = "{user} has reported {target} for {reason} ({info})".format(user=fro, target=target, reason=reason, info=additionalInfo) # Log report in #admin and on discord chat.sendMessage(glob.BOT_NAME, "#admin", adminMsg) log.warning(adminMsg, discord="cm") except exceptions.invalidUserException: msg = "Hello, {} here! You can't report me. I won't forget what you've tried to do. Watch out.".format( glob.BOT_NAME) except exceptions.invalidArgumentsException: msg = "Invalid report command syntax. To report an user, click on it and select 'Report user'." except exceptions.userNotFoundException: msg = "The user you've tried to report doesn't exist." except exceptions.missingReportInfoException: msg = "Please specify the reason of your report." except: raise finally: if msg != "": token = glob.tokens.getTokenFromUsername(fro) if token is not None: if token.irc: chat.sendMessage(glob.BOT_NAME, fro, msg) else: token.enqueue(serverPackets.notification(msg)) return False
def updateRankGlobally(gameMode): gm = gameModes.getGameModeForDB(gameMode) users = glob.db.fetchAll("SELECT user_id FROM osu_user_stats{}".format(gm)) if users is not None: for uid in users: updateRank(uid["user_id"], gameMode) else: log.warning("Fetched None users")
def __init__(self, who, map, checksum, additional_notification): self.who = who self.map = map self.checksum = checksum self.additional_notification = additional_notification log.warning( f"{who} not passed checksum on {map} with checksum {checksum}: {additional_notification}" )
def abort(self): if not self.inProgress: log.warning("MPROOM{}: Match is not in progress!".format(self.matchID)) return self.inProgress = False self.isStarting = False self.resetSlots() self.sendUpdates() glob.streams.broadcast(self.playingStreamName, serverPackets.matchAbort()) glob.streams.dispose(self.playingStreamName) glob.streams.remove(self.playingStreamName) log.info("MPROOM{}: Match aborted".format(self.matchID))
def handle(userToken, _): try: # We don't have the beatmap, we can't spectate if userToken.spectating not in glob.tokens.tokens: raise exceptions.tokenNotFoundException() # Send the packet to host glob.tokens.tokens[userToken.spectating].enqueue(serverPackets.noSongSpectator(userToken.userID)) except exceptions.tokenNotFoundException: # Stop spectating if token not found log.warning("Spectator can't spectate: token not found") userToken.stopSpectating()
def IRCPartChannel(username, channel): """ Handle IRC channel part bancho-side. :param username: username :param channel: channel name :return: IRC return code """ userID = userUtils.getID(username) if not userID: log.warning("{} doesn't exist".format(username)) return return partChannel(userID, channel)
def handle(userToken, packetData): # Read userIDs list packetData = clientPackets.userPanelRequest(packetData) # Process lists with length <= 32 if len(packetData) > 256: log.warning("Received userPanelRequest with length > 256.") return for i in packetData["users"]: # Enqueue userpanel packets relative to this user log.debug("Sending panel for user {}.".format(i)) userToken.enqueue(serverPackets.userPanel(i))
def asyncGet(self) -> None: if not requestsManager.checkArguments(self.request.arguments, ('us', 'ha', 'b')): self.write("-3") return username: Optional[str] = self.get_argument("us", None) userID: int = userUtils.getID(username) if not userUtils.checkLogin(userID, self.get_argument("ha", None), self.getRequestIP()): self.write("-3") return # Get beatmap_idâ„¢ argument b = self.get_argument("b", None) if (b.startswith('a') and not userUtils.isRestricted(userID)): flags = int(b[1:]) if b[1:].isdigit() else None if not flags: self.write("-3") return readable: list[str] = [] if flags & 1 << 0: readable.append("[1] osu! run with -ld") if flags & 1 << 1: readable.append("[2] osu! has a console open") if flags & 1 << 2: readable.append("[4] osu! has extra threads running") if flags & 1 << 3: readable.append("[8] osu! is hqosu! (check #1)") if flags & 1 << 4: readable.append("[16] osu! is hqosu! (check #2)") if flags & 1 << 5: readable.append( "[32] osu! has special launch settings in registry (probably relife)" ) if flags & 1 << 6: readable.append("[64] AQN is loaded (check #1)") if flags & 1 << 7: readable.append("[128] AQN is loaded (check #2)") if flags & 1 << 8: readable.append( "[256] notify_1 was run while out of the editor (AQN sound on program open)" ) # Send our webhook to Discord. log.warning('\n\n'.join([ f'[{username}](https://eggradio.tk/u/{userID}) sent flags **{b}**', '**Breakdown**\n' + '\n'.join(readable), ]), discord='ac') self.write("-3")
def IRCAway(username, message): """ Handle IRC away command bancho-side. :param username: :param message: away message :return: IRC return code """ userID = userUtils.getID(username) if not userID: log.warning("{} doesn't exist".format(username)) return glob.tokens.getTokenFromUserID(userID).awayMessage = message return 305 if message == "" else 306
def asyncGet(self): try: # Get request ip ip = self.getRequestIP() # Check arguments if not requestsManager.checkArguments(self.request.arguments, ["c", "u", "h"]): raise exceptions.invalidArgumentsException(MODULE_NAME) # Get arguments username = self.get_argument("u") password = self.get_argument("h") replayID = self.get_argument("c") isRelaxing = False if int(replayID) < 500000000: isRelaxing = True # Login check userID = userUtils.getID(username) if userID == 0: raise exceptions.loginFailedException(MODULE_NAME, userID) if not userUtils.checkLogin(userID, password, ip): raise exceptions.loginFailedException(MODULE_NAME, username) if userUtils.check2FA(userID, ip): raise exceptions.need2FAException(MODULE_NAME, username, ip) replayData = glob.db.fetch("SELECT scores{relax}.*, users.username AS uname FROM scores{relax} LEFT JOIN users ON scores{relax}.userid = users.id WHERE scores{relax}.id = %s".format(relax="_relax" if isRelaxing else ""), [replayID]) # Increment 'replays watched by others' if needed if replayData is not None: if username != replayData["uname"]: userUtils.incrementReplaysWatched(replayData["userid"], replayData["play_mode"], replayData["mods"]) log.info("Serving replay_{}.osr".format(replayID)) fileName = ".data/replays/replay_{}.osr".format(replayID) if os.path.isfile(fileName): with open(fileName, "rb") as f: fileContent = f.read() self.write(fileContent) else: self.write("") log.warning("Replay {} doesn't exist.".format(replayID)) except exceptions.invalidArgumentsException: pass except exceptions.need2FAException: pass except exceptions.loginFailedException: pass
def IRCDisconnect(username): """ Handle IRC logout bancho-side. Remove token and broadcast logout packet. :param username: username :return: """ token = glob.tokens.getTokenFromUsername(username) if token is None: log.warning("{} doesn't exist".format(username)) return logoutEvent.handle(token) log.info("{} disconnected from IRC".format(username))
def joinChannel(userID=0, channel="", token=None, toIRC=True, force=False): """ Join a channel :param userID: user ID of the user that joins the channel. Optional. token can be used instead. :param token: user token object of user that joins the channel. Optional. userID can be used instead. :param channel: channel name :param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Default: True :param force: whether to allow game clients to join #spect_ and #multi_ channels :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side """ try: # Get token if not defined if token is None: token = glob.tokens.getTokenFromUserID(userID) # Make sure the token exists if token is None: raise exceptions.userNotFoundException else: token = token # Normal channel, do check stuff # Make sure the channel exists if channel not in glob.channels.channels: raise exceptions.channelUnknownException() # Make sure a game client is not trying to join a #multi_ or #spect_ channel manually channelObject = glob.channels.channels[channel] if channelObject.isSpecial and not token.irc and not force: raise exceptions.channelUnknownException() # Add the channel to our joined channel token.joinChannel(channelObject) # Send channel joined (IRC) if glob.irc and not toIRC: glob.ircServer.banchoJoinChannel(token.username, channel) # Console output log.info("{} joined channel {}".format(token.username, channel)) # IRC code return return 0 except exceptions.channelNoPermissionsException: log.warning( "{} attempted to join channel {}, but they have no read permissions" .format(token.username, channel)) return 403 except exceptions.channelUnknownException: log.warning("{} attempted to join an unknown channel ({})".format( token.username, channel)) return 403 except exceptions.userAlreadyInChannelException: log.warning("User {} already in channel {}".format( token.username, channel)) return 403 except exceptions.userNotFoundException: log.warning("User not connected to IRC/Bancho") return 403 # idk
def updateStatsRx(userID, score_): """ Update stats (playcount, total score, ranked score, level bla bla) with data relative to a score object :param userID: :param score_: score object :param beatmap_: beatmap object. Optional. If not passed, it'll be determined by score_. """ # Make sure the user exists if not exists(userID): log.warning("User {} doesn't exist.".format(userID)) return # Get gamemode for db mode = scoreUtils.readableGameMode(score_.gameMode) # Update total score, playcount and play time if score_.playTime is not None: realPlayTime = score_.playTime else: realPlayTime = score_.fullPlayTime glob.db.execute( "UPDATE rx_stats SET total_score_{m}=total_score_{m}+%s, playcount_{m}=playcount_{m}+1, " "playtime_{m} = playtime_{m} + %s " "WHERE id = %s LIMIT 1".format( m=mode ), (score_.score, realPlayTime, userID) ) # Calculate new level and update it updateLevelRX(userID, score_.gameMode) # Update level, accuracy and ranked score only if we have passed the song if score_.passed: # Update ranked score glob.db.execute( "UPDATE rx_stats SET ranked_score_{m}=ranked_score_{m}+%s WHERE id = %s LIMIT 1".format(m=mode), (score_.rankedScoreIncrease, userID) ) # Update accuracy updateAccuracyRX(userID, score_.gameMode) # Update pp updatePPRelax(userID, score_.gameMode)
def handle(self, data): data = super().parseData(data) if data is None: return targetTokens = glob.tokens.getTokenFromUserID(data["userID"], ignoreIRC=True, _all=True) if targetTokens: if glob.banchoConf.config["menuIcon"] == "": log.warning("Tried to test an unknown main menu icon") return for x in targetTokens: x.enqueue( serverPackets.mainMenuIcon( glob.banchoConf.config["menuIcon"]))
def getPP(self): try: # Reset pp self.pp = 0 # Cache map mapsHelper.cacheMap(self.mapPath, self.beatmap) # Calculate pp self.pp = self._runProcess() except PianoError: log.warning("Invalid beatmap {}".format(self.beatmap.beatmapID)) self.pp = 0 finally: return self.pp
def updateStats(userID, score_, *, relax=False): """ Update stats (playcount, total score, ranked score, level bla bla) with data relative to a score object :param userID: :param score_: score object :param relax: if True, update relax stats, otherwise classic stats """ # Make sure the user exists if not exists(userID): log.warning("User {} doesn't exist.".format(userID)) return # Get gamemode for db mode = scoreUtils.readableGameMode(score_.gameMode) table = "users_stats_relax" if relax else "users_stats" # Update total score, playcount and play time if score_.playTime is not None: realPlayTime = score_.playTime else: realPlayTime = score_.fullPlayTime glob.db.execute( "UPDATE {table} SET total_score_{m}=total_score_{m}+%s, " "playcount_{m}=playcount_{m}+1, " "playtime_{m} = playtime_{m} + %s " "WHERE id = %s LIMIT 1".format(table=table, m=mode), (score_.score, realPlayTime, userID)) # Calculate new level and update it updateLevel(userID, score_.gameMode, relax=relax) # Update level, accuracy and ranked score only if we have passed the song if score_.passed: # Update ranked score glob.db.execute( "UPDATE {table} SET ranked_score_{m}=ranked_score_{m}+%s WHERE id = %s LIMIT 1" .format(table=table, m=mode), (score_.rankedScoreIncrease, userID)) # Update accuracy updateAccuracy(userID, score_.gameMode, relax=relax) # Update pp updatePP(userID, score_.gameMode, relax=relax)
def updateStats(userID, score_, beatmap_=None): """ Update stats (playcount, total score, ranked score, level bla bla) with data relative to a score object :param userID: :param score_: score object :param beatmap_: beatmap object. Optional. If not passed, it'll be determined by score_. """ # Make sure the user exists if not exists(userID): log.warning("User {} doesn't exist.".format(userID)) return if beatmap_ is None: beatmap_ = objects.beatmap.beatmap(score_.fileMd5, 0) # Get gamemode for db mode = scoreUtils.readableGameMode(score_.gameMode) # Update total score, playcount and play time realMapLength = beatmap_.hitLength if (score_.mods & mods.DOUBLETIME) > 0: realMapLength //= 1.5 elif (score_.mods & mods.HALFTIME) > 0: realMapLength //= 0.75 glob.db.execute( "UPDATE users_stats SET total_score_{m}=total_score_{m}+%s, playcount_{m}=playcount_{m}+1, " "playtime_{m} = playtime_{m} + %s " "WHERE id = %s LIMIT 1".format(m=mode), (score_.score, realMapLength, userID)) # Calculate new level and update it updateLevel(userID, score_.gameMode) # Update level, accuracy and ranked score only if we have passed the song if score_.passed: # Update ranked score glob.db.execute( "UPDATE users_stats SET ranked_score_{m}=ranked_score_{m}+%s WHERE id = %s LIMIT 1" .format(m=mode), (score_.rankedScoreIncrease, userID)) # Update accuracy updateAccuracy(userID, score_.gameMode) # Update pp updatePP(userID, score_.gameMode)
def IRCJoinChannel(username, channel): """ Handle IRC channel join bancho-side. :param username: username :param channel: channel name :return: IRC return code """ userID = userUtils.getID(username) if not userID: log.warning("{} doesn't exist".format(username)) return # NOTE: This should have also `toIRC` = False` tho, # since we send JOIN message later on ircserver.py. # Will test this later return joinChannel(userID, channel)
def IRCConnect(username): """ Handle IRC login bancho-side. Add token and broadcast login packet. :param username: username :return: """ userID = userUtils.getID(username) if not userID: log.warning("{} doesn't exist".format(username)) return glob.tokens.deleteOldTokens(userID) glob.tokens.addToken(userID, irc=True) glob.streams.broadcast("main", serverPackets.userPanel(userID)) log.info("{} logged in from IRC".format(username))
def updateStats(userID, score_, *, relax=False): """ Update stats (playcount, total score, ranked score, level bla bla) with data relative to a score object :param userID: :param score_: score object :param relax: if True, update relax stats, otherwise classic stats """ # Make sure the user exists if not exists(userID): log.warning("User {} doesn't exist.".format(userID)) return # Get gamemode for db mode = scoreUtils.getGameModeForDB(score_.gameMode) # Update total score, playcount and play time if score_.playTime is not None: realPlayTime = score_.playTime else: realPlayTime = score_.fullPlayTime glob.db.execute( "UPDATE osu_user_stats{m} SET total_score=total_score+%s, " "playcount=playcount+1, " "total_seconds_played = total_seconds_played + %s " "WHERE user_id = %s LIMIT 1".format(m=mode), (score_.score, realPlayTime, userID)) # Calculate new level and update it updateLevel(userID, score_.gameMode, relax=relax) # Update level, accuracy and ranked score only if we have passed the song if score_.passed: # Update ranked score glob.db.execute( "UPDATE osu_user_stats{m} SET ranked_score=ranked_score+%s WHERE user_id = %s LIMIT 1" .format(m=mode), (score_.rankedScoreIncrease, userID)) # Update accuracy updateAccuracy(userID, score_.gameMode, relax=relax) # Update pp updatePP(userID, score_.gameMode, relax=relax) updateRankGlobally(score_.gameMode)
def dbClose(self): tid = threading.get_ident() if self._db is None: log.debug( "Attempted to close a None db connection for thread {} (this thread has no db conn!)" .format(tid)) return try: self._db.close() except Exception as e: log.warning( "Error ({}) while closing db connection for thread {}. Failing silently." .format(e, tid)) pass log.debug( "Closed and destroyed db connection for thread {}".format(tid)) self._db = None
def dbClose(self): tid = threading.get_ident() if self._db is None: log.info( "Closing db connection, but thread {} has no db connection.". format(tid)) return try: self._db.close() except Exception as e: log.warning( "Error ({}) while closing db connection for thread {}. Failing silently." .format(e, tid)) pass log.info( "Closed and destroyed db connection for thread {}".format(tid)) self._db = None
def handle(userToken, packetData): # Read userIDs list packetData = clientPackets.userStatsRequest(packetData) # Process lists with length <= 32 if len(packetData) > 32: log.warning("Received userStatsRequest with length > 32.") return for i in packetData["users"]: log.debug("Sending stats for user {}.".format(i)) # Skip our stats if i == userToken.userID: continue # Enqueue stats packets relative to this user userToken.enqueue(serverPackets.userStats(i))