Beispiel #1
0
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
Beispiel #2
0
    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))
Beispiel #3
0
    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))
Beispiel #4
0
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))
Beispiel #5
0
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))
Beispiel #6
0
    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))
Beispiel #7
0
    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.")
Beispiel #8
0
    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))
Beispiel #9
0
def statsUpdateRequest(p: Player, pr: PacketReader) -> None:
    p.enqueue(packets.userStats(p))
Beispiel #10
0
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
Beispiel #11
0
 async def handle(self, p: Player) -> None:
     p.enqueue(packets.userStats(p))
Beispiel #12
0
        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)
Beispiel #13
0
    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))
Beispiel #14
0
        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
Beispiel #15
0
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)
Beispiel #16
0
 async def handle(self, user):
     enqueue(packets.userStats(user))
Beispiel #17
0
        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)
Beispiel #18
0
            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