diff --git a/ripple/userUtils.py b/ripple/userUtils.py index ab99ecd..894a83c 100644 --- a/ripple/userUtils.py +++ b/ripple/userUtils.py @@ -1,1352 +1,1352 @@ -import time -from _mysql import ProgrammingError - -from common import generalUtils -from common.constants import gameModes -from common.constants import privileges -from common.log import logUtils as log -from common.ripple import passwordUtils, scoreUtils -from objects import glob - - -def getUserStats(userID, gameMode): - """ - Get all user stats relative to `gameMode` - - :param userID: - :param gameMode: game mode number - :return: dictionary with result - """ - modeForDB = gameModes.getGameModeForDB(gameMode) - - # Get stats - stats = glob.db.fetch("""SELECT - ranked_score_{gm} AS rankedScore, - avg_accuracy_{gm} AS accuracy, - playcount_{gm} AS playcount, - total_score_{gm} AS totalScore, - pp_{gm} AS pp - FROM users_stats WHERE id = %s LIMIT 1""".format(gm=modeForDB), [userID]) - - # Get game rank - stats["gameRank"] = getGameRank(userID, gameMode) - - # Return stats + game rank - return stats - -def getUserStatsRx(userID, gameMode): - """ - Get all user stats relative to `gameMode` - - :param userID: - :param gameMode: game mode number - :return: dictionary with result - """ - modeForDB = gameModes.getGameModeForDB(gameMode) - - # Get stats - stats = glob.db.fetch("""SELECT - ranked_score_{gm} AS rankedScore, - avg_accuracy_{gm} AS accuracy, - playcount_{gm} AS playcount, - total_score_{gm} AS totalScore, - pp_{gm}_rx AS pp - FROM users_stats WHERE id = %s LIMIT 1""".format(gm=modeForDB), [userID]) - - # Get game rank - stats["gameRank"] = getGameRankRx(userID, gameMode) - - # Return stats + game rank - return stats - -def getUserStatsAp(userID, gameMode): - """ - Get all user stats relative to `gameMode` - - :param userID: - :param gameMode: game mode number - :return: dictionary with result - """ - modeForDB = gameModes.getGameModeForDB(gameMode) - - # Get stats - stats = glob.db.fetch("""SELECT - ranked_score_{gm} AS rankedScore, - avg_accuracy_{gm} AS accuracy, - playcount_{gm} AS playcount, - total_score_{gm} AS totalScore, - pp_{gm}_rx AS pp - FROM users_stats WHERE id = %s LIMIT 1""".format(gm=modeForDB), [userID]) - - # Get game rank - stats["gameRank"] = getGameRankAp(userID, gameMode) - - # Return stats + game rank - return stats - -def getIDSafe(_safeUsername): - """ - Get user ID from a safe username - :param _safeUsername: safe username - :return: None if the user doesn't exist, else user id - """ - result = glob.db.fetch("SELECT id FROM users WHERE username_safe = %s LIMIT 1", [_safeUsername]) - if result is not None: - return result["id"] - return None - -def getID(username): - """ - Get username's user ID from userID redis cache (if cache hit) - or from db (and cache it for other requests) if cache miss - - :param username: user - :return: user id or 0 if user doesn't exist - """ - # Get userID from redis - usernameSafe = safeUsername(username) - userID = glob.redis.get("ripple:userid_cache:{}".format(usernameSafe)) - - if userID is None: - # If it's not in redis, get it from mysql - userID = getIDSafe(usernameSafe) - - # If it's invalid, return 0 - if userID is None: - return 0 - - # Otherwise, save it in redis and return it - glob.redis.set("ripple:userid_cache:{}".format(usernameSafe), userID, 3600) # expires in 1 hour - return userID - - # Return userid from redis - return int(userID) - -def getUsername(userID): - """ - Get userID's username - - :param userID: user id - :return: username or None - """ - result = glob.db.fetch("SELECT username FROM users WHERE id = %s LIMIT 1", [userID]) - if result is None: - return None - return result["username"] - -def getSafeUsername(userID): - """ - Get userID's safe username - - :param userID: user id - :return: username or None - """ - result = glob.db.fetch("SELECT username_safe FROM users WHERE id = %s LIMIT 1", [userID]) - if result is None: - return None - return result["username_safe"] - -def exists(userID): - """ - Check if given userID exists - - :param userID: user id to check - :return: True if the user exists, else False - """ - return True if glob.db.fetch("SELECT id FROM users WHERE id = %s LIMIT 1", [userID]) is not None else False - -def checkLogin(userID, password, ip=""): - """ - Check userID's login with specified password - - :param userID: user id - :param password: md5 password - :param ip: request IP (used to check active bancho sessions). Optional. - :return: True if user id and password combination is valid, else False - """ - # Check cached bancho session - banchoSession = False - if ip != "": - banchoSession = checkBanchoSession(userID, ip) - - # Return True if there's a bancho session for this user from that ip - if banchoSession: - return True - - # Otherwise, check password - # Get password data - passwordData = glob.db.fetch("SELECT password_md5, salt, password_version FROM users WHERE id = %s LIMIT 1", [userID]) - - # Make sure the query returned something - if passwordData is None: - return False - - # Return valid/invalid based on the password version. - if passwordData["password_version"] == 2: - return passwordUtils.checkNewPassword(password, passwordData["password_md5"]) - if passwordData["password_version"] == 1: - ok = passwordUtils.checkOldPassword(password, passwordData["salt"], passwordData["password_md5"]) - if not ok: - return False - newpass = passwordUtils.genBcrypt(password) - glob.db.execute("UPDATE users SET password_md5=%s, salt='', password_version='2' WHERE id = %s LIMIT 1", [newpass, userID]) - -def getRequiredScoreForLevel(level): - """ - Return score required to reach a level - - :param level: level to reach - :return: required score - """ - if level <= 100: - if level >= 2: - return 5000 / 3 * (4 * (level ** 3) - 3 * (level ** 2) - level) + 1.25 * (1.8 ** (level - 60)) - elif level <= 0 or level == 1: - return 1 # Should be 0, but we get division by 0 below so set to 1 - elif level >= 101: - return 26931190829 + 100000000000 * (level - 100) - -def getLevel(totalScore): - """ - Return level from totalScore - - :param totalScore: total score - :return: level - """ - level = 1 - while True: - # if the level is > 8000, it's probably an endless loop. terminate it. - if level > 8000: - return level - - # Calculate required score - reqScore = getRequiredScoreForLevel(level) - - # Check if this is our level - if totalScore <= reqScore: - # Our level, return it and break - return level - 1 - else: - # Not our level, calculate score for next level - level += 1 - -def updateLevel(userID, gameMode=0, totalScore=0): - """ - Update level in DB for userID relative to gameMode - - :param userID: user id - :param gameMode: game mode number - :param totalScore: new total score - :return: - """ - # Make sure the user exists - # if not exists(userID): - # return - - # Get total score from db if not passed - mode = scoreUtils.readableGameMode(gameMode) - if totalScore == 0: - totalScore = glob.db.fetch( - "SELECT total_score_{m} as total_score FROM users_stats WHERE id = %s LIMIT 1".format(m=mode), [userID]) - if totalScore: - totalScore = totalScore["total_score"] - - # Calculate level from totalScore - level = getLevel(totalScore) - - # Save new level - glob.db.execute("UPDATE users_stats SET level_{m} = %s WHERE id = %s LIMIT 1".format(m=mode), [level, userID]) - -def calculateAccuracy(userID, gameMode): - """ - Calculate accuracy value for userID relative to gameMode - - :param userID: user id - :param gameMode: game mode number - :return: new accuracy - """ - # Select what to sort by - if gameMode == 0: - sortby = "pp" - else: - sortby = "accuracy" - # Get best accuracy scores - bestAccScores = glob.db.fetchAll( - "SELECT accuracy FROM scores WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY " + sortby + " DESC LIMIT 500", - [userID, gameMode]) - - v = 0 - if bestAccScores is not None: - # Calculate weighted accuracy - totalAcc = 0 - divideTotal = 0 - k = 0 - for i in bestAccScores: - add = int((0.95 ** k) * 100) - totalAcc += i["accuracy"] * add - divideTotal += add - k += 1 - # echo "$add - $totalacc - $divideTotal\n" - if divideTotal != 0: - v = totalAcc / divideTotal - else: - v = 0 - return v - -def calculatePP(userID, gameMode): - """ - Calculate userID's total PP for gameMode - - :param userID: user id - :param gameMode: game mode number - :return: total PP - """ - # Get best pp scores - bestPPScores = glob.db.fetchAll( - "SELECT pp FROM scores WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY pp DESC LIMIT 500", - [userID, gameMode]) - - # Calculate weighted PP - totalPP = 0 - if bestPPScores is not None: - k = 0 - for i in bestPPScores: - new = round(round(i["pp"]) * 0.95 ** k) - totalPP += new - k += 1 - - return totalPP - -def calculatePPRelax(userID, gameMode): - """ - Calculate userID's total PP for gameMode - - :param userID: user id - :param gameMode: game mode number - :return: total PP - """ - # Get best pp scores - bestPPScores = glob.db.fetchAll( - "SELECT pp FROM scores_relax WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY pp DESC LIMIT 500", - [userID, gameMode]) - - # Calculate weighted PP - totalPP = 0 - if bestPPScores is not None: - k = 0 - for i in bestPPScores: - new = round(round(i["pp"]) * 0.95 ** k) - totalPP += new - k += 1 - - return totalPP - -def calculatePPAuto(userID, gameMode): - """ - Calculate userID's total PP for gameMode - - :param userID: user id - :param gameMode: game mode number - :return: total PP - """ - # Get best pp scores - bestPPScores = glob.db.fetchAll( - "SELECT pp FROM scores_auto WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY pp DESC LIMIT 500", - [userID, gameMode]) - - # Calculate weighted PP - totalPP = 0 - if bestPPScores is not None: - k = 0 - for i in bestPPScores: - new = round(round(i["pp"]) * 0.95 ** k) - totalPP += new - k += 1 - - - return totalPP -def updateAccuracy(userID, gameMode): - """ - Update accuracy value for userID relative to gameMode in DB - - :param userID: user id - :param gameMode: gameMode number - :return: - """ - newAcc = calculateAccuracy(userID, gameMode) - mode = scoreUtils.readableGameMode(gameMode) - glob.db.execute("UPDATE users_stats SET avg_accuracy_{m} = %s WHERE id = %s LIMIT 1".format(m=mode), - [newAcc, userID]) - -def updatePP(userID, gameMode): - """ - Update userID's pp with new value - - :param userID: user id - :param gameMode: game mode number - """ - # Make sure the user exists - # if not exists(userID): - # return - - # Get new total PP and update db - newPP = calculatePP(userID, gameMode) - mode = scoreUtils.readableGameMode(gameMode) - glob.db.execute("UPDATE users_stats SET pp_{}=%s WHERE id = %s LIMIT 1".format(mode), [newPP, userID]) - - -def updatePPRelax(userID, gameMode): - """ - Update userID's pp with new value - - :param userID: user id - :param gameMode: game mode number - """ - # Make sure the user exists - # if not exists(userID): - # return - - # Get new total PP and update db - newPP = calculatePPRelax(userID, gameMode) - mode = scoreUtils.readableGameMode(gameMode) - glob.db.execute("UPDATE users_stats SET pp_{}_rx=%s WHERE id = %s LIMIT 1".format(mode), [newPP, userID]) - -def updatePPAuto(userID, gameMode): - """ - Update userID's pp with new value - - :param userID: user id - :param gameMode: game mode number - """ - # Make sure the user exists - # if not exists(userID): - # return - - # Get new total PP and update db - newPP = calculatePPAuto(userID, gameMode) - mode = scoreUtils.readableGameMode(gameMode) - glob.db.execute("UPDATE users_stats SET pp_{}_auto=%s WHERE id = %s LIMIT 1".format(mode), [newPP, userID]) - -def updateStats(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 - """ - - # 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 and playcount - glob.db.execute( - "UPDATE users_stats SET total_score_{m}=total_score_{m}+%s, playcount_{m}=playcount_{m}+1 WHERE id = %s LIMIT 1".format( - m=mode), [__score.score, 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 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 - """ - - # 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 and playcount - glob.db.execute( - "UPDATE users_stats SET total_score_{m}=total_score_{m}+%s, playcount_{m}=playcount_{m}+1 WHERE id = %s LIMIT 1".format( - m=mode), [__score.score, 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 - updatePPRelax(userID, __score.gameMode) - -def updateLatestActivity(userID): - """ - Update userID's latest activity to current UNIX time - - :param userID: user id - :return: - """ - glob.db.execute("UPDATE users SET latest_activity = %s WHERE id = %s LIMIT 1", [int(time.time()), userID]) - -def getRankedScore(userID, gameMode): - """ - Get userID's ranked score relative to gameMode - - :param userID: user id - :param gameMode: game mode number - :return: ranked score - """ - mode = scoreUtils.readableGameMode(gameMode) - result = glob.db.fetch("SELECT ranked_score_{} FROM users_stats WHERE id = %s LIMIT 1".format(mode), [userID]) - if result is not None: - return result["ranked_score_{}".format(mode)] - else: - return 0 - -def getPP(userID, gameMode): - """ - Get userID's PP relative to gameMode - - :param userID: user id - :param gameMode: game mode number - :return: pp - """ - - mode = scoreUtils.readableGameMode(gameMode) - result = glob.db.fetch("SELECT pp_{} FROM users_stats WHERE id = %s LIMIT 1".format(mode), [userID]) - if result is not None: - return result["pp_{}".format(mode)] - else: - return 0 - -def incrementReplaysWatched(userID, gameMode): - """ - Increment userID's replays watched by others relative to gameMode - - :param userID: user id - :param gameMode: game mode number - :return: - """ - mode = scoreUtils.readableGameMode(gameMode) - glob.db.execute( - "UPDATE users_stats SET replays_watched_{mode}=replays_watched_{mode}+1 WHERE id = %s LIMIT 1".format( - mode=mode), [userID]) - -def getAqn(userID): - """ - Check if AQN folder was detected for userID - - :param userID: user - :return: True if hax, False if legit - """ - result = glob.db.fetch("SELECT aqn FROM users WHERE id = %s LIMIT 1", [userID]) - if result is not None: - return True if int(result["aqn"]) == 1 else False - else: - return False - -def setAqn(userID, value=1): - """ - Set AQN folder status for userID - - :param userID: user - :param value: new aqn value, default = 1 - :return: - """ - glob.db.fetch("UPDATE users SET aqn = %s WHERE id = %s LIMIT 1", [value, userID]) - -def IPLog(userID, ip): - """ - Log user IP - - :param userID: user id - :param ip: IP address - :return: - """ - glob.db.execute("""INSERT INTO ip_user (userid, ip, occurencies) VALUES (%s, %s, '1') - ON DUPLICATE KEY UPDATE occurencies = occurencies + 1""", [userID, ip]) - -def checkBanchoSession(userID, ip=""): - """ - Return True if there is a bancho session for `userID` from `ip` - If `ip` is an empty string, check if there's a bancho session for that user, from any IP. - - :param userID: user id - :param ip: ip address. Optional. Default: empty string - :return: True if there's an active bancho session, else False - """ - if ip != "": - return glob.redis.sismember("peppy:sessions:{}".format(userID), ip) - else: - return glob.redis.exists("peppy:sessions:{}".format(userID)) - -def is2FAEnabled(userID): - """ - Returns True if 2FA/Google auth 2FA is enable for `userID` - - :userID: user ID - :return: True if 2fa is enabled, else False - """ - return glob.db.fetch("SELECT 2fa_totp.userid FROM 2fa_totp WHERE userid = %(userid)s AND enabled = 1 LIMIT 1", { - "userid": userID - }) is not None - -def check2FA(userID, ip): - """ - Returns True if this IP is untrusted. - Returns always False if 2fa is not enabled on `userID` - - :param userID: user id - :param ip: IP address - :return: True if untrusted, False if trusted or 2fa is disabled. - """ - if not is2FAEnabled(userID): - return False - - result = glob.db.fetch("SELECT id FROM ip_user WHERE userid = %s AND ip = %s", [userID, ip]) - return True if result is None else False - -def isAllowed(userID): - """ - Check if userID is not banned or restricted - - :param userID: user id - :return: True if not banned or restricted, otherwise false. - """ - result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) - if result is not None: - return (result["privileges"] & privileges.USER_NORMAL) and (result["privileges"] & privileges.USER_PUBLIC) - else: - return False - -def isRestricted(userID): - """ - Check if userID is restricted - - :param userID: user id - :return: True if not restricted, otherwise false. - """ - result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) - if result is not None: - return (result["privileges"] & privileges.USER_NORMAL) and not (result["privileges"] & privileges.USER_PUBLIC) - else: - return False - -def isBanned(userID): - """ - Check if userID is banned - - :param userID: user id - :return: True if not banned, otherwise false. - """ - result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) - if result is not None: - return not (result["privileges"] & 3 > 0) - else: - return True - -def isLocked(userID): - """ - Check if userID is locked - - :param userID: user id - :return: True if not locked, otherwise false. - """ - result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) - if result is not None: - return ( - (result["privileges"] & privileges.USER_PUBLIC > 0) and (result["privileges"] & privileges.USER_NORMAL == 0)) - else: - return True - -def ban(userID): - """ - Ban userID - - :param userID: user id - :return: - """ - # Set user as banned in db - banDateTime = int(time.time()) - glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s LIMIT 1", - [~(privileges.USER_NORMAL | privileges.USER_PUBLIC), banDateTime, userID]) - - # Notify bancho about the ban - glob.redis.publish("peppy:ban", userID) - - # Remove the user from global and country leaderboards - removeFromLeaderboard(userID) - -def unban(userID): - """ - Unban userID - - :param userID: user id - :return: - """ - glob.db.execute("UPDATE users SET privileges = privileges | %s, ban_datetime = 0 WHERE id = %s LIMIT 1", - [(privileges.USER_NORMAL | privileges.USER_PUBLIC), userID]) - glob.redis.publish("peppy:ban", userID) - -def restrict(userID): - """ - Restrict userID - - :param userID: user id - :return: - """ - if not isRestricted(userID): - # Set user as restricted in db - banDateTime = int(time.time()) - glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s LIMIT 1", - [~privileges.USER_PUBLIC, banDateTime, userID]) - - # Notify bancho about this ban - glob.redis.publish("peppy:ban", userID) - - # Remove the user from global and country leaderboards - removeFromLeaderboard(userID) - -def unrestrict(userID): - """ - Unrestrict userID. - Same as unban(). - - :param userID: user id - :return: - """ - unban(userID) - -def appendNotes(userID, notes, addNl=True, trackDate=True): - """ - Append `notes` to `userID`'s "notes for CM" - - :param userID: user id - :param notes: text to append - :param addNl: if True, prepend \n to notes. Default: True. - :param trackDate: if True, prepend date and hour to the note. Default: True. - :return: - """ - if trackDate: - notes = "[{}] {}".format(generalUtils.getTimestamp(), notes) - if addNl: - notes = "\n{}".format(notes) - glob.db.execute("UPDATE users SET notes=CONCAT(COALESCE(notes, ''),%s) WHERE id = %s LIMIT 1", [notes, userID]) - -def getPrivileges(userID): - """ - Return `userID`'s privileges - - :param userID: user id - :return: privileges number - """ - result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) - if result is not None: - return result["privileges"] - else: - return 0 - -def getSilenceEnd(userID): - """ - Get userID's **ABSOLUTE** silence end UNIX time - Remember to subtract time.time() if you want to get the actual silence time - - :param userID: user id - :return: UNIX time - """ - return glob.db.fetch("SELECT silence_end FROM users WHERE id = %s LIMIT 1", [userID])["silence_end"] - -def silence(userID, seconds, silenceReason, author = 999): - """ - Silence someone - - :param userID: user id - :param seconds: silence length in seconds - :param silenceReason: silence reason shown on website - :param author: userID of who silenced the user. Default: 999 - :return: - """ - # db qurey - silenceEndTime = int(time.time())+seconds - glob.db.execute("UPDATE users SET silence_end = %s, silence_reason = %s WHERE id = %s LIMIT 1", [silenceEndTime, silenceReason, userID]) - - # Log - targetUsername = getUsername(userID) - # TODO: exists check im drunk rn i need to sleep (stampa piede ubriaco confirmed) - if seconds > 0: - log.rap(author, "has silenced {} for {} seconds for the following reason: \"{}\"".format(targetUsername, seconds, silenceReason), True) - else: - log.rap(author, "has removed {}'s silence".format(targetUsername), True) - -def getTotalScore(userID, gameMode): - """ - Get `userID`'s total score relative to `gameMode` - - :param userID: user id - :param gameMode: game mode number - :return: total score - """ - modeForDB = gameModes.getGameModeForDB(gameMode) - return glob.db.fetch("SELECT total_score_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["total_score_"+modeForDB] - -def getAccuracy(userID, gameMode): - """ - Get `userID`'s average accuracy relative to `gameMode` - - :param userID: user id - :param gameMode: game mode number - :return: accuracy - """ - modeForDB = gameModes.getGameModeForDB(gameMode) - return glob.db.fetch("SELECT avg_accuracy_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["avg_accuracy_"+modeForDB] - -def getGameRank(userID, gameMode): - """ - Get `userID`'s **in-game rank** (eg: #1337) relative to gameMode - - :param userID: user id - :param gameMode: game mode number - :return: game rank - """ - position = glob.redis.zrevrank("ripple:leaderboard:{}".format(gameModes.getGameModeForDB(gameMode)), userID) - if position is None: - return 0 - else: - return int(position) + 1 - -def getGameRankRx(userID, gameMode): - """ - Get `userID`'s **in-game rank** (eg: #1337) relative to gameMode - :param userID: user id - :param gameMode: game mode number - :return: game rank - """ - position = glob.redis.zrevrank("ripple:leaderboard_relax:{}".format(gameModes.getGameModeForDB(gameMode)), userID) - if position is None: - return 0 - else: - return int(position) + 1 - -def getGameRankAp(userID, gameMode): - """ - Get `userID`'s **in-game rank** (eg: #1337) relative to gameMode - :param userID: user id - :param gameMode: game mode number - :return: game rank - """ - position = glob.redis.zrevrank("ripple:leaderboard_auto:{}".format(gameModes.getGameModeForDB(gameMode)), userID) - if position is None: - return 0 - else: - return int(position) + 1 - -def getPlaycount(userID, gameMode): - """ - Get `userID`'s playcount relative to `gameMode` - - :param userID: user id - :param gameMode: game mode number - :return: playcount - """ - modeForDB = gameModes.getGameModeForDB(gameMode) - return glob.db.fetch("SELECT playcount_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["playcount_"+modeForDB] - -def getFriendList(userID): - """ - Get `userID`'s friendlist - - :param userID: user id - :return: list with friends userIDs. [0] if no friends. - """ - # Get friends from db - friends = glob.db.fetchAll("SELECT user2 FROM users_relationships WHERE user1 = %s", [userID]) - - if friends is 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 - - :param userID: user id - :param friendID: new friend - :return: - """ - # 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 = %s AND user2 = %s LIMIT 1", [userID, friendID]) is not None: - return - - # Set new value - glob.db.execute("INSERT INTO users_relationships (user1, user2) VALUES (%s, %s)", [userID, friendID]) - -def removeFriend(userID, friendID): - """ - Remove `friendID` from `userID`'s friend list - - :param userID: user id - :param friendID: old friend - :return: - """ - # 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. ¯\_(ツ)_/¯ - # TODO: LIMIT 1 - glob.db.execute("DELETE FROM users_relationships WHERE user1 = %s AND user2 = %s", [userID, friendID]) - - -def getCountry(userID): - """ - Get `userID`'s country **(two letters)**. - - :param userID: user id - :return: country code (two letters) - """ - return glob.db.fetch("SELECT country FROM users_stats WHERE id = %s LIMIT 1", [userID])["country"] - -def setCountry(userID, country): - """ - Set userID's country - - :param userID: user id - :param country: country letters - :return: - """ - glob.db.execute("UPDATE users_stats SET country = %s WHERE id = %s LIMIT 1", [country, userID]) - -def logIP(userID, ip): - """ - User IP log - USED FOR MULTIACCOUNT DETECTION - - :param userID: user id - :param ip: IP address - :return: - """ - glob.db.execute("""INSERT INTO ip_user (userid, ip, occurencies) VALUES (%s, %s, 1) - ON DUPLICATE KEY UPDATE occurencies = occurencies + 1""", [userID, ip]) - -def saveBanchoSession(userID, ip): - """ - Save userid and ip of this token in redis - Used to cache logins on LETS requests - - :param userID: user ID - :param ip: IP address - :return: - """ - glob.redis.sadd("peppy:sessions:{}".format(userID), ip) - -def deleteBanchoSessions(userID, ip): - """ - Delete this bancho session from redis - - :param userID: user id - :param ip: IP address - :return: - """ - glob.redis.srem("peppy:sessions:{}".format(userID), ip) - -def setPrivileges(userID, priv): - """ - Set userID's privileges in db - - :param userID: user id - :param priv: privileges number - :return: - """ - glob.db.execute("UPDATE users SET privileges = %s WHERE id = %s LIMIT 1", [priv, userID]) - -def getGroupPrivileges(groupName): - """ - Returns the privileges number of a group, by its name - - :param groupName: name of the group - :return: privilege integer or `None` if the group doesn't exist - """ - groupPrivileges = glob.db.fetch("SELECT privileges FROM privileges_groups WHERE name = %s LIMIT 1", [groupName]) - if groupPrivileges is None: - return None - return groupPrivileges["privileges"] - -def isInPrivilegeGroup(userID, groupName): - """ - Check if `userID` is in a privilege group. - Donor privilege is ignored while checking for groups. - - :param userID: user id - :param groupName: privilege group name - :return: True if `userID` is in `groupName`, else False - """ - groupPrivileges = getGroupPrivileges(groupName) - if groupPrivileges is None: - return False - try: - userToken = glob.tokens.getTokenFromUserID(userID) - except AttributeError: - # LETS compatibility - userToken = None - - if userToken is not None: - userPrivileges = userToken.privileges - else: - userPrivileges = getPrivileges(userID) - return userPrivileges & groupPrivileges == groupPrivileges - -def isInAnyPrivilegeGroup(userID, groups): - """ - Checks if a user is in at least one of the specified groups - - :param userID: id of the user - :param groups: groups list or tuple - :return: `True` if `userID` is in at least one of the specified groups, otherwise `False` - """ - userPrivileges = getPrivileges(userID) - return any( - userPrivileges & x == x - for x in ( - getGroupPrivileges(y) for y in groups - ) if x is not None - ) - -def logHardware(userID, hashes, activation = False): - """ - Hardware log - USED FOR MULTIACCOUNT DETECTION - - - :param userID: user id - :param hashes: Peppy's botnet (client data) structure (new line = "|", already split) - [0] osu! version - [1] plain mac addressed, separated by "." - [2] mac addresses hash set - [3] unique ID - [4] disk ID - :param activation: if True, set this hash as used for activation. Default: False. - :return: True if hw is not banned, otherwise false - """ - # Make sure the strings are not empty - for i in hashes[2:5]: - if i == "": - log.warning("Invalid hash set ({}) for user {} in HWID check".format(hashes, userID), "bunk") - return False - - # Run some HWID checks on that user if he is not restricted - if not isRestricted(userID): - # Get username - username = getUsername(userID) - - # Get the list of banned or restricted users that have logged in from this or similar HWID hash set - if hashes[2] == "b4ec3c4334a0249dae95c284ec5983df": - # Running under wine, check by unique id - log.debug("Logging Linux/Mac hardware") - banned = glob.db.fetchAll("""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user - LEFT JOIN users ON users.id = hw_user.userid - WHERE hw_user.userid != %(userid)s - AND hw_user.unique_id = %(uid)s - AND (users.privileges & 3 != 3)""", { - "userid": userID, - "uid": hashes[3], - }) - else: - # Running under windows, do all checks - log.debug("Logging Windows hardware") - banned = glob.db.fetchAll("""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user - LEFT JOIN users ON users.id = hw_user.userid - WHERE hw_user.userid != %(userid)s - AND hw_user.mac = %(mac)s - AND hw_user.unique_id = %(uid)s - AND hw_user.disk_id = %(diskid)s - AND (users.privileges & 3 != 3)""", { - "userid": userID, - "mac": hashes[2], - "uid": hashes[3], - "diskid": hashes[4], - }) - - for i in banned: - # Get the total numbers of logins - total = glob.db.fetch("SELECT COUNT(*) AS count FROM hw_user WHERE userid = %s LIMIT 1", [userID]) - # and make sure it is valid - if total is None: - continue - total = total["count"] - - # Calculate 10% of total - perc = (total*10)/100 - - if i["occurencies"] >= perc: - # If the banned user has logged in more than 10% of the times from this user, restrict this user - restrict(userID) - appendNotes(userID, "Logged in from HWID ({hwid}) used more than 10% from user {banned} ({bannedUserID}), who is banned/restricted.".format( - hwid=hashes[2:5], - banned=i["username"], - bannedUserID=i["userid"] - )) - log.warning("**{user}** ({userID}) has been restricted because he has logged in from HWID _({hwid})_ used more than 10% from banned/restricted user **{banned}** ({bannedUserID}), **possible multiaccount**.".format( - user=username, - userID=userID, - hwid=hashes[2:5], - banned=i["username"], - bannedUserID=i["userid"] - ), "cm") - - # Update hash set occurencies - glob.db.execute(""" - INSERT INTO hw_user (id, userid, mac, unique_id, disk_id, occurencies) VALUES (NULL, %s, %s, %s, %s, 1) - ON DUPLICATE KEY UPDATE occurencies = occurencies + 1 - """, [userID, hashes[2], hashes[3], hashes[4]]) - - # Optionally, set this hash as 'used for activation' - if activation: - glob.db.execute("UPDATE hw_user SET activated = 1 WHERE userid = %s AND mac = %s AND unique_id = %s AND disk_id = %s", [userID, hashes[2], hashes[3], hashes[4]]) - - # Access granted, abbiamo impiegato 3 giorni - # We grant access even in case of login from banned HWID - # because we call restrict() above so there's no need to deny the access. - return True - - -def resetPendingFlag(userID, success=True): - """ - Remove pending flag from an user. - - :param userID: user id - :param success: if True, set USER_PUBLIC and USER_NORMAL flags too - """ - glob.db.execute("UPDATE users SET privileges = privileges & %s WHERE id = %s LIMIT 1", [~privileges.USER_PENDING_VERIFICATION, userID]) - if success: - glob.db.execute("UPDATE users SET privileges = privileges | %s WHERE id = %s LIMIT 1", [(privileges.USER_PUBLIC | privileges.USER_NORMAL), userID]) - -def verifyUser(userID, hashes): - """ - Activate `userID`'s account. - - :param userID: user id - :param hashes: Peppy's botnet (client data) structure (new line = "|", already split) - [0] osu! version - [1] plain mac addressed, separated by "." - [2] mac addresses hash set - [3] unique ID - [4] disk ID - :return: True if verified successfully, else False (multiaccount) - """ - # Check for valid hash set - for i in hashes[2:5]: - if i == "": - log.warning("Invalid hash set ({}) for user {} while verifying the account".format(str(hashes), userID), "bunk") - return False - - # Get username - username = getUsername(userID) - - # Make sure there are no other accounts activated with this exact mac/unique id/hwid - if hashes[2] == "b4ec3c4334a0249dae95c284ec5983df" or hashes[4] == "ffae06fb022871fe9beb58b005c5e21d": - # Running under wine, check only by uniqueid - log.info("{user} ({userID}) ha triggerato Sannino:\n**Full data:** {hashes}\n**Usual wine mac address hash:** b4ec3c4334a0249dae95c284ec5983df\n**Usual wine disk id:** ffae06fb022871fe9beb58b005c5e21d".format(user=username, userID=userID, hashes=hashes), "bunker") - log.debug("Veryfing with Linux/Mac hardware") - match = glob.db.fetchAll("SELECT userid FROM hw_user WHERE unique_id = %(uid)s AND userid != %(userid)s AND activated = 1 LIMIT 1", { - "uid": hashes[3], - "userid": userID - }) - else: - # Running under windows, full check - log.debug("Veryfing with Windows hardware") - match = glob.db.fetchAll("SELECT userid FROM hw_user WHERE mac = %(mac)s AND unique_id = %(uid)s AND disk_id = %(diskid)s AND userid != %(userid)s AND activated = 1 LIMIT 1", { - "mac": hashes[2], - "uid": hashes[3], - "diskid": hashes[4], - "userid": userID - }) - - if match: - # This is a multiaccount, restrict other account and ban this account - - # Get original userID and username (lowest ID) - originalUserID = match[0]["userid"] - originalUsername = getUsername(originalUserID) - - # Ban this user and append notes - ban(userID) # this removes the USER_PENDING_VERIFICATION flag too - appendNotes(userID, "{}'s multiaccount ({}), found HWID match while verifying account ({})".format(originalUsername, originalUserID, hashes[2:5])) - appendNotes(originalUserID, "Has created multiaccount {} ({})".format(username, userID)) - - # Restrict the original - restrict(originalUserID) - - # Discord message - log.warning("User **{originalUsername}** ({originalUserID}) has been restricted because he has created multiaccount **{username}** ({userID}). The multiaccount has been banned.".format( - originalUsername=originalUsername, - originalUserID=originalUserID, - username=username, - userID=userID - ), "cm") - - # Disallow login - return False - else: - # No matches found, set USER_PUBLIC and USER_NORMAL flags and reset USER_PENDING_VERIFICATION flag - resetPendingFlag(userID) - #log.info("User **{}** ({}) has verified his account with hash set _{}_".format(username, userID, hashes[2:5]), "cm") - - # Allow login - return True - -def hasVerifiedHardware(userID): - """ - Checks if `userID` has activated his account through HWID - - :param userID: user id - :return: True if hwid activation data is in db, otherwise False - """ - data = glob.db.fetch("SELECT id FROM hw_user WHERE userid = %s AND activated = 1 LIMIT 1", [userID]) - if data is not None: - return True - return False - -def getDonorExpire(userID): - """ - Return `userID`'s donor expiration UNIX timestamp - - :param userID: user id - :return: donor expiration UNIX timestamp - """ - data = glob.db.fetch("SELECT donor_expire FROM users WHERE id = %s LIMIT 1", [userID]) - if data is not None: - return data["donor_expire"] - return 0 - - -class invalidUsernameError(Exception): - pass - -class usernameAlreadyInUseError(Exception): - pass - -def safeUsername(username): - """ - Return `username`'s safe username - (all lowercase and underscores instead of spaces) - - :param username: unsafe username - :return: safe username - """ - return username.lower().strip().replace(" ", "_") - -def changeUsername(userID=0, oldUsername="", newUsername=""): - """ - Change `userID`'s username to `newUsername` in database - - :param userID: user id. Required only if `oldUsername` is not passed. - :param oldUsername: username. Required only if `userID` is not passed. - :param newUsername: new username. Can't contain spaces and underscores at the same time. - :raise: invalidUsernameError(), usernameAlreadyInUseError() - :return: - """ - # Make sure new username doesn't have mixed spaces and underscores - if " " in newUsername and "_" in newUsername: - raise invalidUsernameError() - - # Get safe username - newUsernameSafe = safeUsername(newUsername) - - # Make sure this username is not already in use - if getIDSafe(newUsernameSafe) is not None: - raise usernameAlreadyInUseError() - - # Get userID or oldUsername - if userID == 0: - userID = getID(oldUsername) - else: - oldUsername = getUsername(userID) - - # Change username - glob.db.execute("UPDATE users SET username = %s, username_safe = %s WHERE id = %s LIMIT 1", [newUsername, newUsernameSafe, userID]) - glob.db.execute("UPDATE users_stats SET username = %s WHERE id = %s LIMIT 1", [newUsername, userID]) - - # Empty redis username cache - # TODO: Le pipe woo woo - glob.redis.delete("ripple:userid_cache:{}".format(safeUsername(oldUsername))) - glob.redis.delete("ripple:change_username_pending:{}".format(userID)) - -def removeFromLeaderboard(userID): - """ - Removes userID from global and country leaderboards. - - :param userID: - :return: - """ - # Remove the user from global and country leaderboards, for every mode - country = getCountry(userID).lower() - for mode in ["std", "taiko", "ctb", "mania"]: - glob.redis.zrem("ripple:leaderboard:{}".format(mode), str(userID)) - if country is not None and len(country) > 0 and country != "xx": - glob.redis.zrem("ripple:leaderboard:{}:{}".format(mode, country), str(userID)) - -def deprecateTelegram2Fa(userID): - """ - Checks whether the user has enabled telegram 2fa on his account. - If so, disables 2fa and returns True. - If not, return False. - - :param userID: id of the user - :return: True if 2fa has been disabled from the account otherwise False - """ - try: - telegram2Fa = glob.db.fetch("SELECT id FROM 2fa_telegram WHERE userid = %s LIMIT 1", (userID,)) - except ProgrammingError: - # The table doesnt exist - return False - - if telegram2Fa is not None: - glob.db.execute("DELETE FROM 2fa_telegram WHERE userid = %s LIMIT 1", (userID,)) - return True - return False - -def unlockAchievement(userID, achievementID): - glob.db.execute("INSERT INTO users_achievements (user_id, achievement_id, `time`) VALUES" - "(%s, %s, %s)", [userID, achievementID, int(time.time())]) - -def getAchievementsVersion(userID): - result = glob.db.fetch("SELECT achievements_version FROM users WHERE id = %s LIMIT 1", [userID]) - if result is None: - return None - return result["achievements_version"] - -def updateAchievementsVersion(userID): - glob.db.execute("UPDATE users SET achievements_version = %s WHERE id = %s LIMIT 1", [ - glob.ACHIEVEMENTS_VERSION, userID - ]) +import time +from _mysql import ProgrammingError + +from common import generalUtils +from common.constants import gameModes +from common.constants import privileges +from common.log import logUtils as log +from common.ripple import passwordUtils, scoreUtils +from objects import glob + + +def getUserStats(userID, gameMode): + """ + Get all user stats relative to `gameMode` + + :param userID: + :param gameMode: game mode number + :return: dictionary with result + """ + modeForDB = gameModes.getGameModeForDB(gameMode) + + # Get stats + stats = glob.db.fetch("""SELECT + ranked_score_{gm} AS rankedScore, + avg_accuracy_{gm} AS accuracy, + playcount_{gm} AS playcount, + total_score_{gm} AS totalScore, + pp_{gm} AS pp + FROM users_stats WHERE id = %s LIMIT 1""".format(gm=modeForDB), [userID]) + + # Get game rank + stats["gameRank"] = getGameRank(userID, gameMode) + + # Return stats + game rank + return stats + +def getUserStatsRx(userID, gameMode): + """ + Get all user stats relative to `gameMode` + + :param userID: + :param gameMode: game mode number + :return: dictionary with result + """ + modeForDB = gameModes.getGameModeForDB(gameMode) + + # Get stats + stats = glob.db.fetch("""SELECT + ranked_score_{gm} AS rankedScore, + avg_accuracy_{gm} AS accuracy, + playcount_{gm} AS playcount, + total_score_{gm} AS totalScore, + pp_{gm}_rx AS pp + FROM users_stats WHERE id = %s LIMIT 1""".format(gm=modeForDB), [userID]) + + # Get game rank + stats["gameRank"] = getGameRankRx(userID, gameMode) + + # Return stats + game rank + return stats + +def getUserStatsAp(userID, gameMode): + """ + Get all user stats relative to `gameMode` + + :param userID: + :param gameMode: game mode number + :return: dictionary with result + """ + modeForDB = gameModes.getGameModeForDB(gameMode) + + # Get stats + stats = glob.db.fetch("""SELECT + ranked_score_{gm} AS rankedScore, + avg_accuracy_{gm} AS accuracy, + playcount_{gm} AS playcount, + total_score_{gm} AS totalScore, + pp_{gm}_auto AS pp + FROM users_stats WHERE id = %s LIMIT 1""".format(gm=modeForDB), [userID]) + + # Get game rank + stats["gameRank"] = getGameRankAp(userID, gameMode) + + # Return stats + game rank + return stats + +def getIDSafe(_safeUsername): + """ + Get user ID from a safe username + :param _safeUsername: safe username + :return: None if the user doesn't exist, else user id + """ + result = glob.db.fetch("SELECT id FROM users WHERE username_safe = %s LIMIT 1", [_safeUsername]) + if result is not None: + return result["id"] + return None + +def getID(username): + """ + Get username's user ID from userID redis cache (if cache hit) + or from db (and cache it for other requests) if cache miss + + :param username: user + :return: user id or 0 if user doesn't exist + """ + # Get userID from redis + usernameSafe = safeUsername(username) + userID = glob.redis.get("ripple:userid_cache:{}".format(usernameSafe)) + + if userID is None: + # If it's not in redis, get it from mysql + userID = getIDSafe(usernameSafe) + + # If it's invalid, return 0 + if userID is None: + return 0 + + # Otherwise, save it in redis and return it + glob.redis.set("ripple:userid_cache:{}".format(usernameSafe), userID, 3600) # expires in 1 hour + return userID + + # Return userid from redis + return int(userID) + +def getUsername(userID): + """ + Get userID's username + + :param userID: user id + :return: username or None + """ + result = glob.db.fetch("SELECT username FROM users WHERE id = %s LIMIT 1", [userID]) + if result is None: + return None + return result["username"] + +def getSafeUsername(userID): + """ + Get userID's safe username + + :param userID: user id + :return: username or None + """ + result = glob.db.fetch("SELECT username_safe FROM users WHERE id = %s LIMIT 1", [userID]) + if result is None: + return None + return result["username_safe"] + +def exists(userID): + """ + Check if given userID exists + + :param userID: user id to check + :return: True if the user exists, else False + """ + return True if glob.db.fetch("SELECT id FROM users WHERE id = %s LIMIT 1", [userID]) is not None else False + +def checkLogin(userID, password, ip=""): + """ + Check userID's login with specified password + + :param userID: user id + :param password: md5 password + :param ip: request IP (used to check active bancho sessions). Optional. + :return: True if user id and password combination is valid, else False + """ + # Check cached bancho session + banchoSession = False + if ip != "": + banchoSession = checkBanchoSession(userID, ip) + + # Return True if there's a bancho session for this user from that ip + if banchoSession: + return True + + # Otherwise, check password + # Get password data + passwordData = glob.db.fetch("SELECT password_md5, salt, password_version FROM users WHERE id = %s LIMIT 1", [userID]) + + # Make sure the query returned something + if passwordData is None: + return False + + # Return valid/invalid based on the password version. + if passwordData["password_version"] == 2: + return passwordUtils.checkNewPassword(password, passwordData["password_md5"]) + if passwordData["password_version"] == 1: + ok = passwordUtils.checkOldPassword(password, passwordData["salt"], passwordData["password_md5"]) + if not ok: + return False + newpass = passwordUtils.genBcrypt(password) + glob.db.execute("UPDATE users SET password_md5=%s, salt='', password_version='2' WHERE id = %s LIMIT 1", [newpass, userID]) + +def getRequiredScoreForLevel(level): + """ + Return score required to reach a level + + :param level: level to reach + :return: required score + """ + if level <= 100: + if level >= 2: + return 5000 / 3 * (4 * (level ** 3) - 3 * (level ** 2) - level) + 1.25 * (1.8 ** (level - 60)) + elif level <= 0 or level == 1: + return 1 # Should be 0, but we get division by 0 below so set to 1 + elif level >= 101: + return 26931190829 + 100000000000 * (level - 100) + +def getLevel(totalScore): + """ + Return level from totalScore + + :param totalScore: total score + :return: level + """ + level = 1 + while True: + # if the level is > 8000, it's probably an endless loop. terminate it. + if level > 8000: + return level + + # Calculate required score + reqScore = getRequiredScoreForLevel(level) + + # Check if this is our level + if totalScore <= reqScore: + # Our level, return it and break + return level - 1 + else: + # Not our level, calculate score for next level + level += 1 + +def updateLevel(userID, gameMode=0, totalScore=0): + """ + Update level in DB for userID relative to gameMode + + :param userID: user id + :param gameMode: game mode number + :param totalScore: new total score + :return: + """ + # Make sure the user exists + # if not exists(userID): + # return + + # Get total score from db if not passed + mode = scoreUtils.readableGameMode(gameMode) + if totalScore == 0: + totalScore = glob.db.fetch( + "SELECT total_score_{m} as total_score FROM users_stats WHERE id = %s LIMIT 1".format(m=mode), [userID]) + if totalScore: + totalScore = totalScore["total_score"] + + # Calculate level from totalScore + level = getLevel(totalScore) + + # Save new level + glob.db.execute("UPDATE users_stats SET level_{m} = %s WHERE id = %s LIMIT 1".format(m=mode), [level, userID]) + +def calculateAccuracy(userID, gameMode): + """ + Calculate accuracy value for userID relative to gameMode + + :param userID: user id + :param gameMode: game mode number + :return: new accuracy + """ + # Select what to sort by + if gameMode == 0: + sortby = "pp" + else: + sortby = "accuracy" + # Get best accuracy scores + bestAccScores = glob.db.fetchAll( + "SELECT accuracy FROM scores WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY " + sortby + " DESC LIMIT 500", + [userID, gameMode]) + + v = 0 + if bestAccScores is not None: + # Calculate weighted accuracy + totalAcc = 0 + divideTotal = 0 + k = 0 + for i in bestAccScores: + add = int((0.95 ** k) * 100) + totalAcc += i["accuracy"] * add + divideTotal += add + k += 1 + # echo "$add - $totalacc - $divideTotal\n" + if divideTotal != 0: + v = totalAcc / divideTotal + else: + v = 0 + return v + +def calculatePP(userID, gameMode): + """ + Calculate userID's total PP for gameMode + + :param userID: user id + :param gameMode: game mode number + :return: total PP + """ + # Get best pp scores + bestPPScores = glob.db.fetchAll( + "SELECT pp FROM scores WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY pp DESC LIMIT 500", + [userID, gameMode]) + + # Calculate weighted PP + totalPP = 0 + if bestPPScores is not None: + k = 0 + for i in bestPPScores: + new = round(round(i["pp"]) * 0.95 ** k) + totalPP += new + k += 1 + + return totalPP + +def calculatePPRelax(userID, gameMode): + """ + Calculate userID's total PP for gameMode + + :param userID: user id + :param gameMode: game mode number + :return: total PP + """ + # Get best pp scores + bestPPScores = glob.db.fetchAll( + "SELECT pp FROM scores_relax WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY pp DESC LIMIT 500", + [userID, gameMode]) + + # Calculate weighted PP + totalPP = 0 + if bestPPScores is not None: + k = 0 + for i in bestPPScores: + new = round(round(i["pp"]) * 0.95 ** k) + totalPP += new + k += 1 + + return totalPP + +def calculatePPAuto(userID, gameMode): + """ + Calculate userID's total PP for gameMode + + :param userID: user id + :param gameMode: game mode number + :return: total PP + """ + # Get best pp scores + bestPPScores = glob.db.fetchAll( + "SELECT pp FROM scores_auto WHERE userid = %s AND play_mode = %s AND completed = 3 ORDER BY pp DESC LIMIT 500", + [userID, gameMode]) + + # Calculate weighted PP + totalPP = 0 + if bestPPScores is not None: + k = 0 + for i in bestPPScores: + new = round(round(i["pp"]) * 0.95 ** k) + totalPP += new + k += 1 + + + return totalPP +def updateAccuracy(userID, gameMode): + """ + Update accuracy value for userID relative to gameMode in DB + + :param userID: user id + :param gameMode: gameMode number + :return: + """ + newAcc = calculateAccuracy(userID, gameMode) + mode = scoreUtils.readableGameMode(gameMode) + glob.db.execute("UPDATE users_stats SET avg_accuracy_{m} = %s WHERE id = %s LIMIT 1".format(m=mode), + [newAcc, userID]) + +def updatePP(userID, gameMode): + """ + Update userID's pp with new value + + :param userID: user id + :param gameMode: game mode number + """ + # Make sure the user exists + # if not exists(userID): + # return + + # Get new total PP and update db + newPP = calculatePP(userID, gameMode) + mode = scoreUtils.readableGameMode(gameMode) + glob.db.execute("UPDATE users_stats SET pp_{}=%s WHERE id = %s LIMIT 1".format(mode), [newPP, userID]) + + +def updatePPRelax(userID, gameMode): + """ + Update userID's pp with new value + + :param userID: user id + :param gameMode: game mode number + """ + # Make sure the user exists + # if not exists(userID): + # return + + # Get new total PP and update db + newPP = calculatePPRelax(userID, gameMode) + mode = scoreUtils.readableGameMode(gameMode) + glob.db.execute("UPDATE users_stats SET pp_{}_rx=%s WHERE id = %s LIMIT 1".format(mode), [newPP, userID]) + +def updatePPAuto(userID, gameMode): + """ + Update userID's pp with new value + + :param userID: user id + :param gameMode: game mode number + """ + # Make sure the user exists + # if not exists(userID): + # return + + # Get new total PP and update db + newPP = calculatePPAuto(userID, gameMode) + mode = scoreUtils.readableGameMode(gameMode) + glob.db.execute("UPDATE users_stats SET pp_{}_auto=%s WHERE id = %s LIMIT 1".format(mode), [newPP, userID]) + +def updateStats(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 + """ + + # 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 and playcount + glob.db.execute( + "UPDATE users_stats SET total_score_{m}=total_score_{m}+%s, playcount_{m}=playcount_{m}+1 WHERE id = %s LIMIT 1".format( + m=mode), [__score.score, 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 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 + """ + + # 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 and playcount + glob.db.execute( + "UPDATE users_stats SET total_score_{m}=total_score_{m}+%s, playcount_{m}=playcount_{m}+1 WHERE id = %s LIMIT 1".format( + m=mode), [__score.score, 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 + updatePPRelax(userID, __score.gameMode) + +def updateLatestActivity(userID): + """ + Update userID's latest activity to current UNIX time + + :param userID: user id + :return: + """ + glob.db.execute("UPDATE users SET latest_activity = %s WHERE id = %s LIMIT 1", [int(time.time()), userID]) + +def getRankedScore(userID, gameMode): + """ + Get userID's ranked score relative to gameMode + + :param userID: user id + :param gameMode: game mode number + :return: ranked score + """ + mode = scoreUtils.readableGameMode(gameMode) + result = glob.db.fetch("SELECT ranked_score_{} FROM users_stats WHERE id = %s LIMIT 1".format(mode), [userID]) + if result is not None: + return result["ranked_score_{}".format(mode)] + else: + return 0 + +def getPP(userID, gameMode): + """ + Get userID's PP relative to gameMode + + :param userID: user id + :param gameMode: game mode number + :return: pp + """ + + mode = scoreUtils.readableGameMode(gameMode) + result = glob.db.fetch("SELECT pp_{} FROM users_stats WHERE id = %s LIMIT 1".format(mode), [userID]) + if result is not None: + return result["pp_{}".format(mode)] + else: + return 0 + +def incrementReplaysWatched(userID, gameMode): + """ + Increment userID's replays watched by others relative to gameMode + + :param userID: user id + :param gameMode: game mode number + :return: + """ + mode = scoreUtils.readableGameMode(gameMode) + glob.db.execute( + "UPDATE users_stats SET replays_watched_{mode}=replays_watched_{mode}+1 WHERE id = %s LIMIT 1".format( + mode=mode), [userID]) + +def getAqn(userID): + """ + Check if AQN folder was detected for userID + + :param userID: user + :return: True if hax, False if legit + """ + result = glob.db.fetch("SELECT aqn FROM users WHERE id = %s LIMIT 1", [userID]) + if result is not None: + return True if int(result["aqn"]) == 1 else False + else: + return False + +def setAqn(userID, value=1): + """ + Set AQN folder status for userID + + :param userID: user + :param value: new aqn value, default = 1 + :return: + """ + glob.db.fetch("UPDATE users SET aqn = %s WHERE id = %s LIMIT 1", [value, userID]) + +def IPLog(userID, ip): + """ + Log user IP + + :param userID: user id + :param ip: IP address + :return: + """ + glob.db.execute("""INSERT INTO ip_user (userid, ip, occurencies) VALUES (%s, %s, '1') + ON DUPLICATE KEY UPDATE occurencies = occurencies + 1""", [userID, ip]) + +def checkBanchoSession(userID, ip=""): + """ + Return True if there is a bancho session for `userID` from `ip` + If `ip` is an empty string, check if there's a bancho session for that user, from any IP. + + :param userID: user id + :param ip: ip address. Optional. Default: empty string + :return: True if there's an active bancho session, else False + """ + if ip != "": + return glob.redis.sismember("peppy:sessions:{}".format(userID), ip) + else: + return glob.redis.exists("peppy:sessions:{}".format(userID)) + +def is2FAEnabled(userID): + """ + Returns True if 2FA/Google auth 2FA is enable for `userID` + + :userID: user ID + :return: True if 2fa is enabled, else False + """ + return glob.db.fetch("SELECT 2fa_totp.userid FROM 2fa_totp WHERE userid = %(userid)s AND enabled = 1 LIMIT 1", { + "userid": userID + }) is not None + +def check2FA(userID, ip): + """ + Returns True if this IP is untrusted. + Returns always False if 2fa is not enabled on `userID` + + :param userID: user id + :param ip: IP address + :return: True if untrusted, False if trusted or 2fa is disabled. + """ + if not is2FAEnabled(userID): + return False + + result = glob.db.fetch("SELECT id FROM ip_user WHERE userid = %s AND ip = %s", [userID, ip]) + return True if result is None else False + +def isAllowed(userID): + """ + Check if userID is not banned or restricted + + :param userID: user id + :return: True if not banned or restricted, otherwise false. + """ + result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) + if result is not None: + return (result["privileges"] & privileges.USER_NORMAL) and (result["privileges"] & privileges.USER_PUBLIC) + else: + return False + +def isRestricted(userID): + """ + Check if userID is restricted + + :param userID: user id + :return: True if not restricted, otherwise false. + """ + result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) + if result is not None: + return (result["privileges"] & privileges.USER_NORMAL) and not (result["privileges"] & privileges.USER_PUBLIC) + else: + return False + +def isBanned(userID): + """ + Check if userID is banned + + :param userID: user id + :return: True if not banned, otherwise false. + """ + result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) + if result is not None: + return not (result["privileges"] & 3 > 0) + else: + return True + +def isLocked(userID): + """ + Check if userID is locked + + :param userID: user id + :return: True if not locked, otherwise false. + """ + result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) + if result is not None: + return ( + (result["privileges"] & privileges.USER_PUBLIC > 0) and (result["privileges"] & privileges.USER_NORMAL == 0)) + else: + return True + +def ban(userID): + """ + Ban userID + + :param userID: user id + :return: + """ + # Set user as banned in db + banDateTime = int(time.time()) + glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s LIMIT 1", + [~(privileges.USER_NORMAL | privileges.USER_PUBLIC), banDateTime, userID]) + + # Notify bancho about the ban + glob.redis.publish("peppy:ban", userID) + + # Remove the user from global and country leaderboards + removeFromLeaderboard(userID) + +def unban(userID): + """ + Unban userID + + :param userID: user id + :return: + """ + glob.db.execute("UPDATE users SET privileges = privileges | %s, ban_datetime = 0 WHERE id = %s LIMIT 1", + [(privileges.USER_NORMAL | privileges.USER_PUBLIC), userID]) + glob.redis.publish("peppy:ban", userID) + +def restrict(userID): + """ + Restrict userID + + :param userID: user id + :return: + """ + if not isRestricted(userID): + # Set user as restricted in db + banDateTime = int(time.time()) + glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s LIMIT 1", + [~privileges.USER_PUBLIC, banDateTime, userID]) + + # Notify bancho about this ban + glob.redis.publish("peppy:ban", userID) + + # Remove the user from global and country leaderboards + removeFromLeaderboard(userID) + +def unrestrict(userID): + """ + Unrestrict userID. + Same as unban(). + + :param userID: user id + :return: + """ + unban(userID) + +def appendNotes(userID, notes, addNl=True, trackDate=True): + """ + Append `notes` to `userID`'s "notes for CM" + + :param userID: user id + :param notes: text to append + :param addNl: if True, prepend \n to notes. Default: True. + :param trackDate: if True, prepend date and hour to the note. Default: True. + :return: + """ + if trackDate: + notes = "[{}] {}".format(generalUtils.getTimestamp(), notes) + if addNl: + notes = "\n{}".format(notes) + glob.db.execute("UPDATE users SET notes=CONCAT(COALESCE(notes, ''),%s) WHERE id = %s LIMIT 1", [notes, userID]) + +def getPrivileges(userID): + """ + Return `userID`'s privileges + + :param userID: user id + :return: privileges number + """ + result = glob.db.fetch("SELECT privileges FROM users WHERE id = %s LIMIT 1", [userID]) + if result is not None: + return result["privileges"] + else: + return 0 + +def getSilenceEnd(userID): + """ + Get userID's **ABSOLUTE** silence end UNIX time + Remember to subtract time.time() if you want to get the actual silence time + + :param userID: user id + :return: UNIX time + """ + return glob.db.fetch("SELECT silence_end FROM users WHERE id = %s LIMIT 1", [userID])["silence_end"] + +def silence(userID, seconds, silenceReason, author = 999): + """ + Silence someone + + :param userID: user id + :param seconds: silence length in seconds + :param silenceReason: silence reason shown on website + :param author: userID of who silenced the user. Default: 999 + :return: + """ + # db qurey + silenceEndTime = int(time.time())+seconds + glob.db.execute("UPDATE users SET silence_end = %s, silence_reason = %s WHERE id = %s LIMIT 1", [silenceEndTime, silenceReason, userID]) + + # Log + targetUsername = getUsername(userID) + # TODO: exists check im drunk rn i need to sleep (stampa piede ubriaco confirmed) + if seconds > 0: + log.rap(author, "has silenced {} for {} seconds for the following reason: \"{}\"".format(targetUsername, seconds, silenceReason), True) + else: + log.rap(author, "has removed {}'s silence".format(targetUsername), True) + +def getTotalScore(userID, gameMode): + """ + Get `userID`'s total score relative to `gameMode` + + :param userID: user id + :param gameMode: game mode number + :return: total score + """ + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT total_score_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["total_score_"+modeForDB] + +def getAccuracy(userID, gameMode): + """ + Get `userID`'s average accuracy relative to `gameMode` + + :param userID: user id + :param gameMode: game mode number + :return: accuracy + """ + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT avg_accuracy_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["avg_accuracy_"+modeForDB] + +def getGameRank(userID, gameMode): + """ + Get `userID`'s **in-game rank** (eg: #1337) relative to gameMode + + :param userID: user id + :param gameMode: game mode number + :return: game rank + """ + position = glob.redis.zrevrank("ripple:leaderboard:{}".format(gameModes.getGameModeForDB(gameMode)), userID) + if position is None: + return 0 + else: + return int(position) + 1 + +def getGameRankRx(userID, gameMode): + """ + Get `userID`'s **in-game rank** (eg: #1337) relative to gameMode + :param userID: user id + :param gameMode: game mode number + :return: game rank + """ + position = glob.redis.zrevrank("ripple:leaderboard_relax:{}".format(gameModes.getGameModeForDB(gameMode)), userID) + if position is None: + return 0 + else: + return int(position) + 1 + +def getGameRankAp(userID, gameMode): + """ + Get `userID`'s **in-game rank** (eg: #1337) relative to gameMode + :param userID: user id + :param gameMode: game mode number + :return: game rank + """ + position = glob.redis.zrevrank("ripple:leaderboard_auto:{}".format(gameModes.getGameModeForDB(gameMode)), userID) + if position is None: + return 0 + else: + return int(position) + 1 + +def getPlaycount(userID, gameMode): + """ + Get `userID`'s playcount relative to `gameMode` + + :param userID: user id + :param gameMode: game mode number + :return: playcount + """ + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT playcount_"+modeForDB+" FROM users_stats WHERE id = %s LIMIT 1", [userID])["playcount_"+modeForDB] + +def getFriendList(userID): + """ + Get `userID`'s friendlist + + :param userID: user id + :return: list with friends userIDs. [0] if no friends. + """ + # Get friends from db + friends = glob.db.fetchAll("SELECT user2 FROM users_relationships WHERE user1 = %s", [userID]) + + if friends is 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 + + :param userID: user id + :param friendID: new friend + :return: + """ + # 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 = %s AND user2 = %s LIMIT 1", [userID, friendID]) is not None: + return + + # Set new value + glob.db.execute("INSERT INTO users_relationships (user1, user2) VALUES (%s, %s)", [userID, friendID]) + +def removeFriend(userID, friendID): + """ + Remove `friendID` from `userID`'s friend list + + :param userID: user id + :param friendID: old friend + :return: + """ + # 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. ¯\_(ツ)_/¯ + # TODO: LIMIT 1 + glob.db.execute("DELETE FROM users_relationships WHERE user1 = %s AND user2 = %s", [userID, friendID]) + + +def getCountry(userID): + """ + Get `userID`'s country **(two letters)**. + + :param userID: user id + :return: country code (two letters) + """ + return glob.db.fetch("SELECT country FROM users_stats WHERE id = %s LIMIT 1", [userID])["country"] + +def setCountry(userID, country): + """ + Set userID's country + + :param userID: user id + :param country: country letters + :return: + """ + glob.db.execute("UPDATE users_stats SET country = %s WHERE id = %s LIMIT 1", [country, userID]) + +def logIP(userID, ip): + """ + User IP log + USED FOR MULTIACCOUNT DETECTION + + :param userID: user id + :param ip: IP address + :return: + """ + glob.db.execute("""INSERT INTO ip_user (userid, ip, occurencies) VALUES (%s, %s, 1) + ON DUPLICATE KEY UPDATE occurencies = occurencies + 1""", [userID, ip]) + +def saveBanchoSession(userID, ip): + """ + Save userid and ip of this token in redis + Used to cache logins on LETS requests + + :param userID: user ID + :param ip: IP address + :return: + """ + glob.redis.sadd("peppy:sessions:{}".format(userID), ip) + +def deleteBanchoSessions(userID, ip): + """ + Delete this bancho session from redis + + :param userID: user id + :param ip: IP address + :return: + """ + glob.redis.srem("peppy:sessions:{}".format(userID), ip) + +def setPrivileges(userID, priv): + """ + Set userID's privileges in db + + :param userID: user id + :param priv: privileges number + :return: + """ + glob.db.execute("UPDATE users SET privileges = %s WHERE id = %s LIMIT 1", [priv, userID]) + +def getGroupPrivileges(groupName): + """ + Returns the privileges number of a group, by its name + + :param groupName: name of the group + :return: privilege integer or `None` if the group doesn't exist + """ + groupPrivileges = glob.db.fetch("SELECT privileges FROM privileges_groups WHERE name = %s LIMIT 1", [groupName]) + if groupPrivileges is None: + return None + return groupPrivileges["privileges"] + +def isInPrivilegeGroup(userID, groupName): + """ + Check if `userID` is in a privilege group. + Donor privilege is ignored while checking for groups. + + :param userID: user id + :param groupName: privilege group name + :return: True if `userID` is in `groupName`, else False + """ + groupPrivileges = getGroupPrivileges(groupName) + if groupPrivileges is None: + return False + try: + userToken = glob.tokens.getTokenFromUserID(userID) + except AttributeError: + # LETS compatibility + userToken = None + + if userToken is not None: + userPrivileges = userToken.privileges + else: + userPrivileges = getPrivileges(userID) + return userPrivileges & groupPrivileges == groupPrivileges + +def isInAnyPrivilegeGroup(userID, groups): + """ + Checks if a user is in at least one of the specified groups + + :param userID: id of the user + :param groups: groups list or tuple + :return: `True` if `userID` is in at least one of the specified groups, otherwise `False` + """ + userPrivileges = getPrivileges(userID) + return any( + userPrivileges & x == x + for x in ( + getGroupPrivileges(y) for y in groups + ) if x is not None + ) + +def logHardware(userID, hashes, activation = False): + """ + Hardware log + USED FOR MULTIACCOUNT DETECTION + + + :param userID: user id + :param hashes: Peppy's botnet (client data) structure (new line = "|", already split) + [0] osu! version + [1] plain mac addressed, separated by "." + [2] mac addresses hash set + [3] unique ID + [4] disk ID + :param activation: if True, set this hash as used for activation. Default: False. + :return: True if hw is not banned, otherwise false + """ + # Make sure the strings are not empty + for i in hashes[2:5]: + if i == "": + log.warning("Invalid hash set ({}) for user {} in HWID check".format(hashes, userID), "bunk") + return False + + # Run some HWID checks on that user if he is not restricted + if not isRestricted(userID): + # Get username + username = getUsername(userID) + + # Get the list of banned or restricted users that have logged in from this or similar HWID hash set + if hashes[2] == "b4ec3c4334a0249dae95c284ec5983df": + # Running under wine, check by unique id + log.debug("Logging Linux/Mac hardware") + banned = glob.db.fetchAll("""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user + LEFT JOIN users ON users.id = hw_user.userid + WHERE hw_user.userid != %(userid)s + AND hw_user.unique_id = %(uid)s + AND (users.privileges & 3 != 3)""", { + "userid": userID, + "uid": hashes[3], + }) + else: + # Running under windows, do all checks + log.debug("Logging Windows hardware") + banned = glob.db.fetchAll("""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user + LEFT JOIN users ON users.id = hw_user.userid + WHERE hw_user.userid != %(userid)s + AND hw_user.mac = %(mac)s + AND hw_user.unique_id = %(uid)s + AND hw_user.disk_id = %(diskid)s + AND (users.privileges & 3 != 3)""", { + "userid": userID, + "mac": hashes[2], + "uid": hashes[3], + "diskid": hashes[4], + }) + + for i in banned: + # Get the total numbers of logins + total = glob.db.fetch("SELECT COUNT(*) AS count FROM hw_user WHERE userid = %s LIMIT 1", [userID]) + # and make sure it is valid + if total is None: + continue + total = total["count"] + + # Calculate 10% of total + perc = (total*10)/100 + + if i["occurencies"] >= perc: + # If the banned user has logged in more than 10% of the times from this user, restrict this user + restrict(userID) + appendNotes(userID, "Logged in from HWID ({hwid}) used more than 10% from user {banned} ({bannedUserID}), who is banned/restricted.".format( + hwid=hashes[2:5], + banned=i["username"], + bannedUserID=i["userid"] + )) + log.warning("**{user}** ({userID}) has been restricted because he has logged in from HWID _({hwid})_ used more than 10% from banned/restricted user **{banned}** ({bannedUserID}), **possible multiaccount**.".format( + user=username, + userID=userID, + hwid=hashes[2:5], + banned=i["username"], + bannedUserID=i["userid"] + ), "cm") + + # Update hash set occurencies + glob.db.execute(""" + INSERT INTO hw_user (id, userid, mac, unique_id, disk_id, occurencies) VALUES (NULL, %s, %s, %s, %s, 1) + ON DUPLICATE KEY UPDATE occurencies = occurencies + 1 + """, [userID, hashes[2], hashes[3], hashes[4]]) + + # Optionally, set this hash as 'used for activation' + if activation: + glob.db.execute("UPDATE hw_user SET activated = 1 WHERE userid = %s AND mac = %s AND unique_id = %s AND disk_id = %s", [userID, hashes[2], hashes[3], hashes[4]]) + + # Access granted, abbiamo impiegato 3 giorni + # We grant access even in case of login from banned HWID + # because we call restrict() above so there's no need to deny the access. + return True + + +def resetPendingFlag(userID, success=True): + """ + Remove pending flag from an user. + + :param userID: user id + :param success: if True, set USER_PUBLIC and USER_NORMAL flags too + """ + glob.db.execute("UPDATE users SET privileges = privileges & %s WHERE id = %s LIMIT 1", [~privileges.USER_PENDING_VERIFICATION, userID]) + if success: + glob.db.execute("UPDATE users SET privileges = privileges | %s WHERE id = %s LIMIT 1", [(privileges.USER_PUBLIC | privileges.USER_NORMAL), userID]) + +def verifyUser(userID, hashes): + """ + Activate `userID`'s account. + + :param userID: user id + :param hashes: Peppy's botnet (client data) structure (new line = "|", already split) + [0] osu! version + [1] plain mac addressed, separated by "." + [2] mac addresses hash set + [3] unique ID + [4] disk ID + :return: True if verified successfully, else False (multiaccount) + """ + # Check for valid hash set + for i in hashes[2:5]: + if i == "": + log.warning("Invalid hash set ({}) for user {} while verifying the account".format(str(hashes), userID), "bunk") + return False + + # Get username + username = getUsername(userID) + + # Make sure there are no other accounts activated with this exact mac/unique id/hwid + if hashes[2] == "b4ec3c4334a0249dae95c284ec5983df" or hashes[4] == "ffae06fb022871fe9beb58b005c5e21d": + # Running under wine, check only by uniqueid + log.info("{user} ({userID}) ha triggerato Sannino:\n**Full data:** {hashes}\n**Usual wine mac address hash:** b4ec3c4334a0249dae95c284ec5983df\n**Usual wine disk id:** ffae06fb022871fe9beb58b005c5e21d".format(user=username, userID=userID, hashes=hashes), "bunker") + log.debug("Veryfing with Linux/Mac hardware") + match = glob.db.fetchAll("SELECT userid FROM hw_user WHERE unique_id = %(uid)s AND userid != %(userid)s AND activated = 1 LIMIT 1", { + "uid": hashes[3], + "userid": userID + }) + else: + # Running under windows, full check + log.debug("Veryfing with Windows hardware") + match = glob.db.fetchAll("SELECT userid FROM hw_user WHERE mac = %(mac)s AND unique_id = %(uid)s AND disk_id = %(diskid)s AND userid != %(userid)s AND activated = 1 LIMIT 1", { + "mac": hashes[2], + "uid": hashes[3], + "diskid": hashes[4], + "userid": userID + }) + + if match: + # This is a multiaccount, restrict other account and ban this account + + # Get original userID and username (lowest ID) + originalUserID = match[0]["userid"] + originalUsername = getUsername(originalUserID) + + # Ban this user and append notes + ban(userID) # this removes the USER_PENDING_VERIFICATION flag too + appendNotes(userID, "{}'s multiaccount ({}), found HWID match while verifying account ({})".format(originalUsername, originalUserID, hashes[2:5])) + appendNotes(originalUserID, "Has created multiaccount {} ({})".format(username, userID)) + + # Restrict the original + restrict(originalUserID) + + # Discord message + log.warning("User **{originalUsername}** ({originalUserID}) has been restricted because he has created multiaccount **{username}** ({userID}). The multiaccount has been banned.".format( + originalUsername=originalUsername, + originalUserID=originalUserID, + username=username, + userID=userID + ), "cm") + + # Disallow login + return False + else: + # No matches found, set USER_PUBLIC and USER_NORMAL flags and reset USER_PENDING_VERIFICATION flag + resetPendingFlag(userID) + #log.info("User **{}** ({}) has verified his account with hash set _{}_".format(username, userID, hashes[2:5]), "cm") + + # Allow login + return True + +def hasVerifiedHardware(userID): + """ + Checks if `userID` has activated his account through HWID + + :param userID: user id + :return: True if hwid activation data is in db, otherwise False + """ + data = glob.db.fetch("SELECT id FROM hw_user WHERE userid = %s AND activated = 1 LIMIT 1", [userID]) + if data is not None: + return True + return False + +def getDonorExpire(userID): + """ + Return `userID`'s donor expiration UNIX timestamp + + :param userID: user id + :return: donor expiration UNIX timestamp + """ + data = glob.db.fetch("SELECT donor_expire FROM users WHERE id = %s LIMIT 1", [userID]) + if data is not None: + return data["donor_expire"] + return 0 + + +class invalidUsernameError(Exception): + pass + +class usernameAlreadyInUseError(Exception): + pass + +def safeUsername(username): + """ + Return `username`'s safe username + (all lowercase and underscores instead of spaces) + + :param username: unsafe username + :return: safe username + """ + return username.lower().strip().replace(" ", "_") + +def changeUsername(userID=0, oldUsername="", newUsername=""): + """ + Change `userID`'s username to `newUsername` in database + + :param userID: user id. Required only if `oldUsername` is not passed. + :param oldUsername: username. Required only if `userID` is not passed. + :param newUsername: new username. Can't contain spaces and underscores at the same time. + :raise: invalidUsernameError(), usernameAlreadyInUseError() + :return: + """ + # Make sure new username doesn't have mixed spaces and underscores + if " " in newUsername and "_" in newUsername: + raise invalidUsernameError() + + # Get safe username + newUsernameSafe = safeUsername(newUsername) + + # Make sure this username is not already in use + if getIDSafe(newUsernameSafe) is not None: + raise usernameAlreadyInUseError() + + # Get userID or oldUsername + if userID == 0: + userID = getID(oldUsername) + else: + oldUsername = getUsername(userID) + + # Change username + glob.db.execute("UPDATE users SET username = %s, username_safe = %s WHERE id = %s LIMIT 1", [newUsername, newUsernameSafe, userID]) + glob.db.execute("UPDATE users_stats SET username = %s WHERE id = %s LIMIT 1", [newUsername, userID]) + + # Empty redis username cache + # TODO: Le pipe woo woo + glob.redis.delete("ripple:userid_cache:{}".format(safeUsername(oldUsername))) + glob.redis.delete("ripple:change_username_pending:{}".format(userID)) + +def removeFromLeaderboard(userID): + """ + Removes userID from global and country leaderboards. + + :param userID: + :return: + """ + # Remove the user from global and country leaderboards, for every mode + country = getCountry(userID).lower() + for mode in ["std", "taiko", "ctb", "mania"]: + glob.redis.zrem("ripple:leaderboard:{}".format(mode), str(userID)) + if country is not None and len(country) > 0 and country != "xx": + glob.redis.zrem("ripple:leaderboard:{}:{}".format(mode, country), str(userID)) + +def deprecateTelegram2Fa(userID): + """ + Checks whether the user has enabled telegram 2fa on his account. + If so, disables 2fa and returns True. + If not, return False. + + :param userID: id of the user + :return: True if 2fa has been disabled from the account otherwise False + """ + try: + telegram2Fa = glob.db.fetch("SELECT id FROM 2fa_telegram WHERE userid = %s LIMIT 1", (userID,)) + except ProgrammingError: + # The table doesnt exist + return False + + if telegram2Fa is not None: + glob.db.execute("DELETE FROM 2fa_telegram WHERE userid = %s LIMIT 1", (userID,)) + return True + return False + +def unlockAchievement(userID, achievementID): + glob.db.execute("INSERT INTO users_achievements (user_id, achievement_id, `time`) VALUES" + "(%s, %s, %s)", [userID, achievementID, int(time.time())]) + +def getAchievementsVersion(userID): + result = glob.db.fetch("SELECT achievements_version FROM users WHERE id = %s LIMIT 1", [userID]) + if result is None: + return None + return result["achievements_version"] + +def updateAchievementsVersion(userID): + glob.db.execute("UPDATE users SET achievements_version = %s WHERE id = %s LIMIT 1", [ + glob.ACHIEVEMENTS_VERSION, userID + ])