async def getScores(p: Player, conn: AsyncConnection) -> Optional[bytes]: isdecimal_n = partial(_isdecimal, _negative=True) # make sure all int args are integral if not all(isdecimal_n(conn.args[k]) for k in ('mods', 'v', 'm', 'i')): return b'-1|false' map_md5 = conn.args['c'] mods = int(conn.args['mods']) mode = GameMode.from_params(int(conn.args['m']), mods) map_set_id = int(conn.args['i']) rank_type = RankingType(int(conn.args['v'])) # attempt to update their stats if their # gm/gm-affecting-mods change at all. if mode != p.status.mode: p.status.mods = mods p.status.mode = mode glob.players.enqueue(packets.userStats(p)) table = mode.sql_table scoring = 'pp' if mode >= GameMode.rx_std else 'score' if not (bmap := await Beatmap.from_md5(map_md5, map_set_id)): # couldn't find in db or at osu! api by md5. # check if we have the map in our db (by filename). filename = conn.args['f'].replace('+', ' ') if not (re := regexes.mapfile.match(unquote(filename))): log(f'Requested invalid file - {filename}.', Ansi.LRED) return
async def handle(self, p: Player) -> None: unrestrcted_ids = [p.id for p in glob.players.unrestricted] is_online = lambda o: o in unrestrcted_ids and o != p.id for online in filter(is_online, self.user_ids): if t := glob.players.get(id=online): p.enqueue(packets.userStats(t))
async def handle(self, p: Player) -> None: # update the user's status. p.status.update(self.action, self.info_text, self.map_md5, self.mods, self.mode, self.map_id) # broadcast it to all online players. glob.players.enqueue(packets.userStats(p))
def statsRequest(p: Player, pr: PacketReader) -> None: if len(pr.data) < 6: return userIDs = pr.read(osuTypes.i32_list) is_online = lambda o: o in glob.players.ids and o != p.id for online in filter(is_online, userIDs): target = glob.players.get_by_id(online) p.enqueue(packets.userStats(target))
def readStatus(p: Player, pr: PacketReader) -> None: data = pr.read( osuTypes.u8, # actionType osuTypes.string, # infotext osuTypes.string, # beatmap md5 osuTypes.u32, # mods osuTypes.u8, # gamemode osuTypes.i32 # beatmapid ) p.status.update(*data) p.rx = p.status.mods & Mods.RELAX > 0 glob.players.enqueue(packets.userStats(p))
async def handle(self, p: Player) -> None: # update the user's status. p.status.action = Action(self.action) p.status.info_text = self.info_text p.status.map_md5 = self.map_md5 p.status.mods = Mods(self.mods) if p.status.mods & Mods.RELAX: self.mode += 4 elif p.status.mods & Mods.AUTOPILOT: self.mode = 7 p.status.mode = GameMode(self.mode) p.status.map_id = self.map_id # broadcast it to all online players. glob.players.enqueue(packets.userStats(p))
def update_stats(self, gm: GameMode = GameMode.vn_std) -> None: table = 'scores_rx' if gm >= 4 else 'scores_vn' res = glob.db.fetchall( f'SELECT s.pp, s.acc FROM {table} s ' 'LEFT JOIN maps m ON s.map_md5 = m.md5 ' 'WHERE s.userid = %s AND s.game_mode = %s ' 'AND s.status = 2 AND m.status IN (1, 2) ' 'ORDER BY s.pp DESC LIMIT 100', [self.id, gm - (4 if gm >= 4 else 0)]) if not res: return # ? # Update the user's stats ingame, then update db. self.stats[gm].plays += 1 self.stats[gm].pp = sum( round(round(row['pp']) * 0.95**i) for i, row in enumerate(res)) self.stats[gm].acc = sum([row['acc'] for row in res][:50]) / min( 50, len(res)) / 100.0 glob.db.execute( 'UPDATE stats SET pp_{0:sql} = %s, ' 'plays_{0:sql} = plays_{0:sql} + 1, ' 'acc_{0:sql} = %s WHERE id = %s'.format(gm), [self.stats[gm].pp, self.stats[gm].acc, self.id]) # Calculate rank. res = glob.db.fetch( 'SELECT COUNT(*) AS c FROM stats ' 'LEFT JOIN users USING(id) ' f'WHERE pp_{gm:sql} > %s ' 'AND priv & 1', [self.stats[gm].pp]) self.stats[gm].rank = res['c'] + 1 self.enqueue(packets.userStats(self)) printlog(f"Updated {self}'s {gm!r} stats.")
async def update_stats(self, mode: GameMode = GameMode.vn_std) -> None: """Update a player's stats in-game and in sql.""" table = mode.sql_table res = await glob.db.fetchall( f'SELECT s.pp, s.acc FROM {table} s ' 'LEFT JOIN maps m ON s.map_md5 = m.md5 ' 'WHERE s.userid = %s AND s.mode = %s ' 'AND s.status = 2 AND m.status IN (1, 2) ' 'ORDER BY s.pp DESC LIMIT 100', [self.id, mode.as_vanilla]) if not res: return # ? # update the user's stats in-game, then update db. self.stats[mode].plays += 1 self.stats[mode].acc = sum([row['acc'] for row in res][:50]) / min(50, len(res)) self.stats[mode].pp = round( sum(row['pp'] * 0.95**i for i, row in enumerate(res))) await glob.db.execute( 'UPDATE stats SET pp_{0:sql} = %s, ' 'plays_{0:sql} = plays_{0:sql} + 1, ' 'acc_{0:sql} = %s WHERE id = %s'.format(mode), [self.stats[mode].pp, self.stats[mode].acc, self.id]) # calculate rank. res = await glob.db.fetch( 'SELECT COUNT(*) AS c FROM stats ' 'LEFT JOIN users USING(id) ' f'WHERE pp_{mode:sql} > %s ' 'AND priv & 1', [self.stats[mode].pp]) self.stats[mode].rank = res['c'] + 1 self.enqueue(packets.userStats(self))
def statsUpdateRequest(p: Player, pr: PacketReader) -> None: p.enqueue(packets.userStats(p))
async def login(): headers = request.headers # request headers, used for things such as user ip and agent if 'User-Agent' not in headers or headers['User-Agent'] != 'osu!': # request isn't sent from osu client, return nothing return if 'osu-token' not in headers: # sometimes a login request will be a re-connect attempt, in which case they will already have a token, if not: login the user data = await request.data # request data, used to get info such as username to login the user info = data.decode().split( '\n')[:-1] # format data so we can use it easier username = info[0] pw = info[1].encode( ) # password in md5 form, we will use this to compare against db's stored bcrypt later user = await glob.db.fetch( 'SELECT id, pw, country, name FROM users WHERE name = %s', [username]) if not user: # ensure user actually exists before attempting to do anything else log(f'User {username} does not exist.', Ansi.LRED) resp = await make_response(packets.userID(-1)) resp.headers['cho-token'] = 'no' return resp bcache = glob.cache[ 'bcrypt'] # get our cached bcrypts to potentially enhance speed pw_bcrypt = user['pw'].encode() if pw_bcrypt in bcache: if pw != bcache[ pw_bcrypt]: # compare provided md5 with the stored (cached) bcrypt to ensure they have provided the correct password log( f"{username}'s login attempt failed: provided an incorrect password", Ansi.LRED) resp = await make_response(packets.userID(-1)) resp.headers['cho-token'] = 'no' return resp else: if not bcrypt.checkpw( pw, pw_bcrypt ): # compare provided md5 with the stored bcrypt to ensure they have provided the correct password log( f"{username}'s login attempt failed: provided an incorrect password", Ansi.LRED) resp = await make_response(packets.userID(-1)) resp.headers['cho-token'] = 'no' return resp bcache[pw_bcrypt] = pw # cache pw for future token = uuid.uuid4() # generate token for client to use as auth ucache = glob.cache['user'] if str(token) not in ucache: ucache[str(token)] = user[ 'id'] # cache token to use outside of this request data = bytearray(packets.userID( user['id'])) # initiate login by providing the user's id data += packets.protocolVersion(19) # no clue what this does data += packets.banchoPrivileges( 1 << 4) # force priv to developer for now data += ( packets.userPresence(user) + packets.userStats(user) ) # provide user & other user's presence/stats (for f9 + user stats) data += packets.notification( f'Welcome to Asahi v{glob.version}' ) # send notification as indicator they've logged in iguess data += packets.channelInfoEnd() # no clue what this does either resp = await make_response(bytes(data)) resp.headers['cho-token'] = token log(f'{username} successfully logged in.', Ansi.GREEN) return resp # if we have made it this far then it's a reconnect attempt with token already provided user_token = headers['osu-token'] # client-provided token tcache = glob.cache[ 'user'] # get token/userid cache to see if we need to relog the user or not if user_token not in tcache: # user is logged in but token is not found? most likely a restart so we force a reconnection return packets.restartServer(0) user = await glob.db.fetch( 'SELECT id, pw, country, name FROM users WHERE id = %s', [tcache[user_token]]) body = await request.body # handle any packets the client has sent | doesn't really work **for now** for packet in BanchoPacketReader(body, glob.packets): await packet.handle(user) log(f'Handled packet {packet}') resp = await make_response(b'') resp.headers['Content-Type'] = 'text/html; charset=UTF-8' # ? return resp
async def handle(self, p: Player) -> None: p.enqueue(packets.userStats(p))
data += packets.channelInfo(*c.basic_info) # fetch some of the player's # information from sql to be cached. await p.achievements_from_sql() await p.stats_from_sql_full() await p.friends_from_sql() if glob.config.server_build: # update their country data with # the IP from the login request. await p.fetch_geoloc(ip) # update our new player's stats, and broadcast them. user_data = (packets.userPresence(p) + packets.userStats(p)) data += user_data # o for online, or other for o in glob.players: # enqueue us to them o.enqueue(user_data) # enqueue them to us. data += packets.userPresence(o) data += packets.userStats(o) data += packets.mainMenuIcon() data += packets.friendsList(*p.friends) data += packets.silenceEnd(p.remaining_silence)
async def handle(self, p: Player) -> None: is_online = lambda o: o in glob.players.ids and o != p.id for online in filter(is_online, self.user_ids): if t := await glob.players.get(id=online): p.enqueue(packets.userStats(t))
ip = headers['X-Real-IP'] reader = database.Reader('ext/geoloc.mmdb') geoloc = reader.city(ip) country, user['lat'], user['lon'] = (geoloc.country.iso_code, geoloc.location.latitude, geoloc.location.longitude) user['country'] = country_codes[country] await glob.db.execute('UPDATE users SET country = %s WHERE id = %s', [country.lower(), user['id']]) # update country code in db friends = {row['user2'] async for row in glob.db.iterall('SELECT user2 FROM friends WHERE user1 = %s', [user['id']])} # select all friends from db ucache = glob.cache['user'] if str(token) not in ucache: ucache[str(token)] = user['id'] # cache token to use outside of this request data = bytearray(packets.userID(user['id'])) # initiate login by providing the user's id data += packets.protocolVersion(19) # no clue what this does data += packets.banchoPrivileges(1 << 4) # force priv to developer for now data += (packets.userPresence(user) + packets.userStats(user)) # provide user & other user's presence/stats (for f9 + user stats) data += packets.notification(f'Welcome to Asahi v{glob.version}') # send notification as indicator they've logged in i guess data += packets.channelInfoEnd() # no clue what this does either data += packets.menuIcon() # set main menu icon data += packets.friends(*friends) # send user friend list data += packets.silenceEnd(0) # force to 0 for now since silences arent a thing #data += packets.sendMessage(user['name'], 'test message lol so cool', user['name'], user['id']) # test message # add user to cache? pcache = glob.players pcache.append(user) for p in pcache: # enqueue other users to client data += (packets.userPresence(p) + packets.userStats(p)) resp = await make_response(bytes(data)) resp.headers['cho-token'] = token
async def osuSubmitModularSelector(conn: AsyncConnection) -> Optional[bytes]: mp_args = conn.multipart_args # Parse our score data into a score obj. s = await Score.from_submission( mp_args['score'], mp_args['iv'], mp_args['osuver'], mp_args['pass'] ) if not s: log('Failed to parse a score - invalid format.', Ansi.LRED) return b'error: no' elif not s.player: # Player is not online, return nothing so that their # client will retry submission when they log in. return elif not s.bmap: # Map does not exist, most likely unsubmitted. return b'error: no' elif s.bmap.status == RankedStatus.Pending: # XXX: Perhaps will accept in the future, return b'error: no' # not now though. # attempt to update their stats if their # gm/gm-affecting-mods change at all. if s.mode != s.player.status.mode: s.player.status.mods = s.mods s.player.status.mode = s.mode glob.players.enqueue(packets.userStats(s.player)) table = s.mode.sql_table # Check for score duplicates # TODO: might need to improve? res = await glob.db.fetch( f'SELECT 1 FROM {table} WHERE mode = %s ' 'AND map_md5 = %s AND userid = %s AND mods = %s ' 'AND score = %s', [ s.mode.as_vanilla, s.bmap.md5, s.player.id, int(s.mods), s.score ] ) if res: log(f'{s.player} submitted a duplicate score.', Ansi.LYELLOW) return b'error: no' time_elapsed = mp_args['st' if s.passed else 'ft'] if not time_elapsed.isdecimal(): return s.time_elapsed = int(time_elapsed) if 'i' in conn.files: breakpoint() if not s.player.priv & Privileges.Whitelisted: # Get the PP cap for the current context. pp_cap = autoban_pp[s.mode][s.mods & Mods.FLASHLIGHT != 0] if s.pp > pp_cap: log(f'{s.player} banned for submitting ' f'{s.pp:.2f} score on gm {s.mode!r}.', Ansi.LRED) await s.player.ban(glob.bot, f'[{s.mode!r}] autoban @ {s.pp:.2f}') return b'error: ban' if s.status == SubmissionStatus.BEST: # Our score is our best score. # Update any preexisting personal best # records with SubmissionStatus.SUBMITTED. await glob.db.execute( f'UPDATE {table} SET status = 1 ' 'WHERE status = 2 AND map_md5 = %s ' 'AND userid = %s AND mode = %s', [s.bmap.md5, s.player.id, s.mode.as_vanilla] ) s.id = await glob.db.execute( f'INSERT INTO {table} VALUES (NULL, ' '%s, %s, %s, %s, %s, %s, ' '%s, %s, %s, %s, %s, %s, ' '%s, %s, %s, %s, ' '%s, %s, %s, %s)', [ s.bmap.md5, s.score, s.pp, s.acc, s.max_combo, int(s.mods), s.n300, s.n100, s.n50, s.nmiss, s.ngeki, s.nkatu, s.grade, int(s.status), s.mode.as_vanilla, s.play_time, s.time_elapsed, s.client_flags, s.player.id, s.perfect ] ) if s.status != SubmissionStatus.FAILED: # All submitted plays should have a replay. # If not, they may be using a score submitter. if 'score' not in conn.files or conn.files['score'] == b'\r\n': log(f'{s.player} submitted a score without a replay!', Ansi.LRED) await s.player.ban(glob.bot, f'submitted score with no replay') else: # TODO: the replay is currently sent from the osu! # client compressed with LZMA; this compression can # be improved pretty decently by serializing it # manually, so we'll probably do that in the future. async with aiofiles.open(f'.data/osr/{s.id}.osr', 'wb') as f: await f.write(conn.files['score']) """ Update the user's & beatmap's stats """ # get the current stats, and take a # shallow copy for the response charts. stats = s.player.gm_stats prev_stats = copy.copy(stats) # update playtime & plays stats.playtime += s.time_elapsed / 1000 stats.plays += 1 s.bmap.plays += 1 if s.passed: s.bmap.passes += 1 # update max combo if s.max_combo > stats.max_combo: stats.max_combo = s.max_combo # update total score stats.tscore += s.score # if this is our (new) best play on # the map, update our ranked score. if s.status == SubmissionStatus.BEST \ and s.bmap.status in (RankedStatus.Ranked, RankedStatus.Approved): # add our new ranked score. additive = s.score if s.prev_best: # we previously had a score, so remove # it's score from our ranked score. additive -= s.prev_best.score stats.rscore += additive # update user with new stats await glob.db.execute( 'UPDATE stats SET rscore_{0:sql} = %s, ' 'tscore_{0:sql} = %s, playtime_{0:sql} = %s, ' 'plays_{0:sql} = %s, maxcombo_{0:sql} = %s ' 'WHERE id = %s'.format(s.mode), [ stats.rscore, stats.tscore, stats.playtime, stats.plays, stats.max_combo, s.player.id ] ) # update beatmap with new stats await glob.db.execute( 'UPDATE maps SET plays = %s, ' 'passes = %s WHERE md5 = %s', [s.bmap.plays, s.bmap.passes, s.bmap.md5] ) if s.status == SubmissionStatus.BEST and s.rank == 1 \ and (announce_chan := glob.channels['#announce']): # Announce the user's #1 score. prev_n1 = await glob.db.fetch( 'SELECT u.id, name FROM users u ' f'LEFT JOIN {table} s ON u.id = s.userid ' 'WHERE s.map_md5 = %s AND s.mode = %s ' 'AND s.status = 2 ORDER BY pp DESC LIMIT 1, 1', [s.bmap.md5, s.mode.as_vanilla] ) performance = f'{s.pp:.2f}pp' if s.pp else f'{s.score}' ann = [f'\x01ACTION achieved #1 on {s.bmap.embed} {s.mods!r} with {s.acc:.2f}% for {performance}.'] if prev_n1: # If there was previously a score on the map, add old #1. ann.append('(Previously: [https://osu.ppy.sh/u/{id} {name}])'.format(**prev_n1)) await announce_chan.send(s.player, ' '.join(ann), to_client=True)
async def handle(self, user): enqueue(packets.userStats(user))
data += packets.channelInfo(*c.basic_info) # fetch some of the player's # information from sql to be cached. await p.stats_from_sql_full() await p.friends_from_sql() if glob.config.server_build: # update their country data with # the IP from the login request. await p.fetch_geoloc(ip) # update our new player's stats, and broadcast them. user_data = ( packets.userPresence(p) + packets.userStats(p) ) data += user_data # o for online, or other for o in glob.players: # enqueue us to them o.enqueue(user_data) # enqueue them to us. data += packets.userPresence(o) data += packets.userStats(o) data += packets.mainMenuIcon() data += packets.friendsList(*p.friends)
data.extend(packets.channelJoin(c.name)) data.extend(packets.channelInfo(*c.basic_info)) # fetch some of the player's # information from sql to be cached. await p.stats_from_sql_full() await p.friends_from_sql() if glob.config.server_build: # update their country data with # the IP from the login request. await p.fetch_geoloc(ip) # update our new player's stats, and broadcast them. user_data = (packets.userPresence(p) + packets.userStats(p)) data.extend(user_data) # o for online, or other for o in glob.players: # enqueue us to them o.enqueue(user_data) # enqueue them to us. data.extend(packets.userPresence(o) + packets.userStats(o)) data.extend(packets.mainMenuIcon() + packets.friendsList(*p.friends) + packets.silenceEnd(p.remaining_silence)) # thank u osu for doing this by username rather than id